]> dgit.raspbian.org Git - python-pysam.git/commitdiff
New upstream version 0.23.0+ds
authorÉtienne Mollier <emollier@debian.org>
Wed, 5 Feb 2025 20:00:32 +0000 (21:00 +0100)
committerÉtienne Mollier <emollier@debian.org>
Wed, 5 Feb 2025 20:00:32 +0000 (21:00 +0100)
195 files changed:
.cirrus.yml
AUTHORS
INSTALL
MANIFEST.in
NEWS
README.rst
bcftools/HMM.h
bcftools/LICENSE
bcftools/abuf.c
bcftools/abuf.c.pysam.c
bcftools/bam2bcf.c
bcftools/bam2bcf.c.pysam.c
bcftools/bam2bcf.h
bcftools/bam2bcf_edlib.c [new file with mode: 0644]
bcftools/bam2bcf_edlib.c.pysam.c [new file with mode: 0644]
bcftools/bam2bcf_iaux.c
bcftools/bam2bcf_iaux.c.pysam.c
bcftools/bam2bcf_indel.c
bcftools/bam2bcf_indel.c.pysam.c
bcftools/bcftools.h
bcftools/call.h
bcftools/consensus.c
bcftools/consensus.c.pysam.c
bcftools/convert.c
bcftools/convert.c.pysam.c
bcftools/convert.h
bcftools/csq.c
bcftools/csq.c.pysam.c
bcftools/edlib.c [new file with mode: 0644]
bcftools/edlib.c.pysam.c [new file with mode: 0644]
bcftools/edlib.h [new file with mode: 0644]
bcftools/filter.c
bcftools/filter.c.pysam.c
bcftools/filter.h
bcftools/gff.c
bcftools/gff.c.pysam.c
bcftools/gff.h
bcftools/gvcf.c
bcftools/gvcf.c.pysam.c
bcftools/main.c
bcftools/main.c.pysam.c
bcftools/mcall.c
bcftools/mcall.c.pysam.c
bcftools/mpileup.c
bcftools/mpileup.c.pysam.c
bcftools/read_consensus.c
bcftools/read_consensus.c.pysam.c
bcftools/regidx.h
bcftools/reheader.c
bcftools/reheader.c.pysam.c
bcftools/str_finder.c
bcftools/str_finder.c.pysam.c
bcftools/str_finder.h
bcftools/variantkey.h
bcftools/vcfannotate.c
bcftools/vcfannotate.c.pysam.c
bcftools/vcfbuf.c
bcftools/vcfbuf.c.pysam.c
bcftools/vcfbuf.h
bcftools/vcfcall.c
bcftools/vcfcall.c.pysam.c
bcftools/vcfcnv.c
bcftools/vcfcnv.c.pysam.c
bcftools/vcfconcat.c
bcftools/vcfconcat.c.pysam.c
bcftools/vcfconvert.c
bcftools/vcfconvert.c.pysam.c
bcftools/vcffilter.c
bcftools/vcffilter.c.pysam.c
bcftools/vcfgtcheck.c
bcftools/vcfgtcheck.c.pysam.c
bcftools/vcfhead.c
bcftools/vcfhead.c.pysam.c
bcftools/vcfindex.c
bcftools/vcfindex.c.pysam.c
bcftools/vcfisec.c
bcftools/vcfisec.c.pysam.c
bcftools/vcfmerge.c
bcftools/vcfmerge.c.pysam.c
bcftools/vcfnorm.c
bcftools/vcfnorm.c.pysam.c
bcftools/vcfplugin.c
bcftools/vcfplugin.c.pysam.c
bcftools/vcfquery.c
bcftools/vcfquery.c.pysam.c
bcftools/vcfroh.c
bcftools/vcfroh.c.pysam.c
bcftools/vcfsom.c
bcftools/vcfsom.c.pysam.c
bcftools/vcfsort.c
bcftools/vcfsort.c.pysam.c
bcftools/vcfstats.c
bcftools/vcfstats.c.pysam.c
bcftools/vcfview.c
bcftools/vcfview.c.pysam.c
bcftools/version.c
bcftools/version.c.pysam.c
bcftools/version.sh
devtools/check-platform.sh [new file with mode: 0755]
devtools/environment-dev.yaml
devtools/import.py
doc/conf.py
doc/developer.rst
doc/faq.rst
doc/index.rst
doc/installation.rst
doc/usage.rst
pysam/bcftools.py
pysam/libcalignedsegment.pyi
pysam/libcalignedsegment.pyx
pysam/libcalignmentfile.pyi
pysam/libcbcf.pyi
pysam/libcbcf.pyx
pysam/libcbgzf.pxd [new file with mode: 0644]
pysam/libcbgzf.pyx
pysam/libchtslib.pxd
pysam/libchtslib.pyi
pysam/libchtslib.pyx
pysam/libctabix.pyi
pysam/libcutils.pyx
pysam/samtools.py
pysam/utils.py
pysam/version.h
pysam/version.py
samtools/LICENSE
samtools/README
samtools/amplicon_stats.c
samtools/amplicon_stats.c.pysam.c
samtools/bam2depth.c
samtools/bam2depth.c.pysam.c
samtools/bam_ampliconclip.c
samtools/bam_ampliconclip.c.pysam.c
samtools/bam_ampliconclip.h
samtools/bam_cat.c
samtools/bam_cat.c.pysam.c
samtools/bam_consensus.c
samtools/bam_consensus.c.pysam.c
samtools/bam_fastq.c
samtools/bam_fastq.c.pysam.c
samtools/bam_import.c
samtools/bam_import.c.pysam.c
samtools/bam_index.c
samtools/bam_index.c.pysam.c
samtools/bam_markdup.c
samtools/bam_markdup.c.pysam.c
samtools/bam_mate.c
samtools/bam_mate.c.pysam.c
samtools/bam_md.c
samtools/bam_md.c.pysam.c
samtools/bam_plbuf.h
samtools/bam_plcmd.c
samtools/bam_plcmd.c.pysam.c
samtools/bam_sort.c
samtools/bam_sort.c.pysam.c
samtools/bam_split.c
samtools/bam_split.c.pysam.c
samtools/bamtk.c
samtools/bamtk.c.pysam.c
samtools/bedcov.c
samtools/bedcov.c.pysam.c
samtools/bedidx.c
samtools/bedidx.c.pysam.c
samtools/coverage.c
samtools/coverage.c.pysam.c
samtools/faidx.c
samtools/faidx.c.pysam.c
samtools/lz4/lz4.c
samtools/lz4/lz4.c.pysam.c
samtools/phase.c
samtools/phase.c.pysam.c
samtools/reset.c
samtools/reset.c.pysam.c
samtools/sam_utils.c
samtools/sam_utils.c.pysam.c
samtools/sam_view.c
samtools/sam_view.c.pysam.c
samtools/stats.c
samtools/stats.c.pysam.c
samtools/tmp_file.c
samtools/tmp_file.c.pysam.c
samtools/version.sh
setup.py
tests/AlignedSegment_test.py
tests/AlignmentFileHeader_test.py
tests/TestUtils.py
tests/VariantFile_bench.py
tests/VariantRecord_test.py
tests/cbcf_data/Makefile
tests/pysam_data/MM-chebi.sam
tests/pysam_data/MM-explicit.sam [new file with mode: 0644]
tests/pysam_data/MM-multi.sam
tests/pysam_data/Makefile
tests/samtools_test.py
tests/tabix_data/Makefile
tests/typechecking_test.py [new file with mode: 0644]

index 2b20b311cc8c984ac53272be117c9c504c12b7c9..1ad3445f0e724ba6924102ef84ecd538d97fe401 100644 (file)
@@ -11,15 +11,15 @@ build_wheels_task:
         - name: Build ARM Linux py3.6-9 wheels
           env:
             CIBW_BUILD: "cp36-* cp37-* cp38-* cp39-*"
-        - name: Build ARM Linux py3.10-12 wheels
+        - name: Build ARM Linux py3.10-13 wheels
           env:
-            CIBW_BUILD: "cp310-* cp311-* cp312-*"
+            CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-*"
 
     - name: Build ARM macOS wheels
       macos_instance:
         image: ghcr.io/cirruslabs/macos-sonoma-base:latest
       env:
-        CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-*"
+        CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*"
 
   alias: build_wheels
 
@@ -32,11 +32,14 @@ build_wheels_task:
     CIBW_SKIP: "*-musllinux_*"
     CIBW_BUILD_VERBOSITY: 1
 
+    # Avoid linking with non-system library libdeflate.dylib
+    CIBW_ENVIRONMENT_MACOS: HTSLIB_CONFIGURE_OPTIONS="--without-libdeflate"
+
     CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
 
   install_script: |
     python3 -m venv $VENV
-    pip3 install cibuildwheel==2.17.0
+    pip3 install cibuildwheel==2.22.0
 
   build_script: |
     cibuildwheel
diff --git a/AUTHORS b/AUTHORS
index 4e9c5eb5f897a0684a49fe0ba1927068ef9720d7..091cd91ab7e9e9ee65ff4603a2b3f18cc656b680 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,12 +1,12 @@
-Many people have contributed to pysam. The list of github contributors
+Many people have contributed to pysam. The list of GitHub contributors
 is the best place to get a full list of authors and their contributions.
 The list and summary below is a out-of-date and represents the earlier
 stages of the project.
 
 List of contributors:
 
-Andreas Heger, Tildon Grant Belgard, Florian Finkernagel, Leo
-Goodstadt, Martin Goodson all contributed code to pysam.
+Andreas Heger, Tildon Grant Belgard, Florian Finkernagel,
+Leo Goodstadt, Martin Goodson all contributed code to pysam.
 
 John Marshall has been looking after pysam and its community for
 several years, as well as making many code contributions and improving
@@ -17,7 +17,7 @@ reader/writer in htslib.
 
 Gerton Lunter provided a validating VCF parser.
 
-Marcel Martin implemented python 3 compatibility.
+Marcel Martin implemented Python 3 compatibility and added type hints.
 Ben Schiller contributed a Windows compatible clone.
 
 The sources in the directory samtools are from the samtools project:
@@ -31,13 +31,3 @@ Bob Handsaker from the Broad Institute is a major contributor to the
 SAM/BAM specification. He designed and implemented the BGZF format, the
 underlying indexable compression format for the BAM format. BGZF does
 not support arithmetic between file offsets.
-
-Jue Ruan for the Beijing Genome Institute designed and implemented the
-RAZF format, an alternative indexable compression format. RAZF supports
-arithmetic between file offsets, at the cost of increased index file
-size and the full compatibility with gzip. RAZF is optional and only
-used in `faidx' for indexing RAZF compressed fasta files.
-
-Colin Hercus updated novo2sam.pl to support gapped alignment by
-novoalign.
-
diff --git a/INSTALL b/INSTALL
index 5016dcc75ba4af2cf4b07e3521460ce0ecf8f336..6dd0163f1e7d58dbd90ce28907a1971d78f0ad7b 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -5,7 +5,7 @@ http://pysam.readthedocs.io/en/latest/installation.html
 Installing pysam
 ================
 
-Pysam can be installed through conda_, pypi_ and from the repository.
+Pysam can be installed through conda_, PyPI_ and from the repository.
 The recommended way to install pysam is through conda/bioconda.
 
 Conda installation
@@ -22,7 +22,7 @@ makes sure that dependencies are installed. Also, compilation flags
 will be set automatically, which will potentially save a lot of
 trouble on OS X.
 
-Pypi installation
+PyPI installation
 =================
 
 Pysam provides a python interface to the functionality contained
@@ -32,7 +32,7 @@ can be combined, ``builtin`` and ``external``.
 Builtin
 -------
 
-The typical installation will be through pypi_::
+The typical installation will be through PyPI_::
 
    pip install pysam
 
@@ -86,7 +86,7 @@ To install from repository, type::
 
     python setup.py install
 
-For compilation options, see the section on Pypi installation above.
+For compilation options, see the section on PyPI installation above.
 
 Requirements
 ============
index 5711f090280ffb08f0e12515c41d8b78c22c2e16..47e77029cd4561ad0e8ccb8bf7e2c8ec0c4b7cc5 100644 (file)
@@ -19,8 +19,7 @@ exclude pysam/config.py
 
 include win32/*.[ch]
 
-# exclude tests from pypi tar-ball - they
-# require additional data
+# exclude tests from sdist tarball as they require additional data
 prune tests/
 
 # samtools
@@ -41,7 +40,7 @@ recursive-include htslib *.[ch]
 exclude htslib/*config*.h
 
 include htslib/configure.ac htslib/m4/*.m4 htslib/*.in
-include htslib/configure htslib/version.sh
+include htslib/configure htslib/config.guess htslib/config.sub htslib/version.sh
 include htslib/Makefile htslib/*.mk
 exclude htslib/config.mk htslib/htscodecs.mk
 
diff --git a/NEWS b/NEWS
index 83ee80324d652f4f3236aec7ff927848c03d41cf..a952ef47d845c80df9fe12e741a8e279e33d188b 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,82 @@
 .. An online version of the release history can be found here:
 .. http://pysam.readthedocs.io/en/latest/release.html
 
+Release 0.23.0
+==============
+
+.. rubric:: 5 February 2025
+
+This pysam release wraps htslib/samtools/bcftools 1.21 (PR #1310).
+
+It has been tested with Python versions 3.6 through 3.13, and wheels are
+available via PyPI_ for all of those Python versions. Python versions 3.6
+through 3.8 are end-of-life; particularly if you use pysam with one of
+these versions, please vote in the version survey at issue #1230.
+
+This is expected to be the last release supporting Python 3.6 and 3.7.
+
+Bugs fixed:
+
+* Fix :func:`pysam.samtools.command(save_stdout=filename) <.samtools.command>`
+  redirection of output to a file. (#677, reported by Haowen Zhang and analysed
+  by Youri Hoogstrate)
+
+* Reinstate HTTPS/S3/GCS support in pre-built Linux wheels when used on Debian
+  and Ubuntu: these wheels now work around the discrepancy between Red Hat and
+  Debian CA Certificate file locations. (#1257, #1268, reported by Daisie Huang
+  and Li Tai Fang)
+
+* Calling :meth:`.VariantHeader.new_record` repeatedly with the same
+  :obj:`samples` object now sets GT correctly every time. (#1308, reported
+  by Arthur Gymer)
+
+* Correct the exception produced when :meth:`.AlignedSegment.set_tags` is
+  used with an invalid value type. (#1233, PR #1235, reported by Weisheng Wu
+  and Marcus Stoiber)
+
+* Many type hinting corrections. (#1298, #1316, PR #1296, PR #1306, PR #1313,
+  PR #1315, thanks to Victor Epain, Arthur Gymer, @mshunjan, and Matt Stone)
+
+* The undocumented :func:`!pysam.samtools.import_` alias for invoking
+  ``samtools import`` has been removed;
+  use :func:`pysam.samtools.fqimport() <.samtools.command>` instead.
+
+* Corrections to several test data files to account for HTSlib 1.20 and later's
+  improved validity checking. (#1291, reported by David Seifert; etc)
+
+New functionality:
+
+* :meth:`.AlignedSegment.get_aligned_pairs` now optionally returns the
+  associated CIGAR operator in each position tuple. (#1292, PR #1294,
+  thanks to Lara Fuhrmann and Ivan Blagoev Topolsky)
+
+* New :meth:`AlignmentFile.flush() <.HTSFile.flush>` and
+  :meth:`VariantFile.flush() <.HTSFile.flush>` methods for flushing buffered
+  output to streams. (#1299, requested by @blex-max)
+
+* Improved :class:`str() <str>` for :class:`.AlignedSegment` now displays
+  reference sequence names when they are available and uses ``#N`` only when
+  necessary. (#1318, requested by Liu)
+
+* Implement :func:`repr` for :class:`.AlignedSegment` so that it displays the
+  most useful fields. (PR #1267, thanks to Marcel Martin)
+
+* Pysam's tests can now run in parallel. (#1284, reported by Yuri Victorovich)
+
+Documentation improvements:
+
+* More complete documentation of invoking SAMtools and BCFtools subcommands
+  via Pysam. (#1096, #1241, PR #1261, PR #1275, #1323, et al, reported by
+  Seung-been "Steven" Lee, Robert Baldwin, Michael Hall, Indraniel Das,
+  and @shokrofont)
+
+* Use the Pysam names for CIGAR operators rather than internal C names.
+  (#1255, reported by Ilya Shlyakhter)
+
+* Mention :meth:`AlignmentFile.fetch("*") <.AlignmentFile.fetch>` in the
+  FAQ entry on fetching unmapped reads. (#424, reported by Ben Weisburd)
+
+
 Release 0.22.1
 ==============
 
@@ -11,24 +87,24 @@ Bugfix release, which still wraps htslib/samtools/bcftools 1.18.
 Bugs fixed:
 
 * Preserve all header field tags defined in the SAM specification (notably TP)
-  in :meth:`.AlignmentHeader.from_dict` and :meth:`.AlignmentHeader.to_dict`
+  in :meth:`.AlignmentHeader.from_dict` and :meth:`.AlignmentHeader.to_dict`.
   (#1237, PR #1238, thanks to Tim Fennell and Nils Homer)
 
 * Adjust HTSlib's Makefile so that ``make distclean`` no longer tries to
-  rebuild the htscodecs configury (PR #1247, reported by Nicola Soranzo)
+  rebuild the htscodecs configury. (PR #1247, reported by Nicola Soranzo)
 
 * Reinstate S3 support in pre-built Linux wheels: support for this protocol
-  was inadvertently omitted from the pre-built 0.22.0 wheels on Linux
+  was inadvertently omitted from the pre-built 0.22.0 wheels on Linux.
   (#1249, #1277, etc varying circumstances; likely it is this that was
   reported by Mathew Baines, Benjamin Sargsyan, et al)
 
-* Add missing :attr:`.AlignedSegment.is_mapped` etc properties to type stubs
+* Add missing :attr:`.AlignedSegment.is_mapped` etc properties to type stubs.
   (PR #1273, thanks to Matt Stone)
 
-* Fix off-by-one NamedTupleProxy, :class:`.asBed`, etc array bounds check
+* Fix off-by-one NamedTupleProxy, :class:`.asBed`, etc array bounds check.
   (#1279, reported by Dan Bolser)
 
-* Make pysam's klib headers compatible with C++ (reported by Martin Grigorov)
+* Make pysam's klib headers compatible with C++. (reported by Martin Grigorov)
 
 
 Release 0.22.0
@@ -39,7 +115,7 @@ Release 0.22.0
 This pysam release wraps htslib/samtools/bcftools 1.18 (PR #1208).
 
 It has been tested with Python versions 3.6 through 3.12, and wheels are
-available via pypi_ for all of those Python versions. Python versions 3.6
+available via PyPI_ for all of those Python versions. Python versions 3.6
 and 3.7 are end-of-life; particularly if you use pysam with either of
 these versions, please vote in the version survey at issue #1230.
 
@@ -47,54 +123,54 @@ The final pysam release that supported Python 2.7 was v0.20.0.
 
 Bugs fixed:
 
-* Remove Cython from runtime dependencies (PR #1186, thanks to Nicola Soranzo,
+* Remove Cython from runtime dependencies. (PR #1186, thanks to Nicola Soranzo,
   also reported by Arya Massarat in PR #1194)
 
-* Miscellaneous dependency improvements (PR #1216, #1217, PR #1218, PR #1219,
+* Miscellaneous dependency improvements. (PR #1216, #1217, PR #1218, PR #1219,
   thanks to Martin Larralde and Arthur Vigil)
 
 * Suppress spurious "Could not retrieve index file" message when opening an
-  AlignmentFile (#939, #1214, reported by ChengYong Tham and Sebastian Röner)
+  AlignmentFile. (#939, #1214, reported by ChengYong Tham and Sebastian Röner)
 
-* Propagate SAM parsing errors encounted in :meth:`.AlignedSegment.fromstring`
+* Propagate SAM parsing errors encounted in :meth:`.AlignedSegment.fromstring`.
   (#1196, reported by DV Klopfenstein)
 
 * Accept invalid MD:A tagged fields produced by HTSeq instead of crashing
   in :meth:`AlignedSegment.get_aligned_pairs(with_seq=True)
-  <.AlignedSegment.get_aligned_pairs>` (#1226, reported by Isaac Vock)
+  <.AlignedSegment.get_aligned_pairs>`. (#1226, reported by Isaac Vock)
 
-* Fix multiarch macOS CI builds by removing brewed liblzma (#1205, reported
+* Fix multiarch macOS CI builds by removing brewed liblzma. (#1205, reported
   by Till Hartmann)
 
-* Fix :attr:`.VariantRecordSample.alleles` type hint (#1179, reported by
+* Fix :attr:`.VariantRecordSample.alleles` type hint. (#1179, reported by
   David Seifert)
 
 New functionality:
 
 * Add optional :meth:`HTSFile.seek(..., whence) <.HTSFile.seek>` parameter
-  and clarify which functions use libc.SEEK_SET vs io.SEEK_SET
+  and clarify which functions use libc.SEEK_SET vs io.SEEK_SET.
   (#1185, requested by luyulin)
 
-* File handling improvements in samtools & bcftools commands (should improve
+* File handling improvements in samtools & bcftools commands. (should improve
   #1193 and #1195, reported by Rob Bierman and Sam Chorlton)
 
-* Improve :class:`.FastxFile` performance (PR #1227, thanks to Fabian Klötzl
+* Improve :class:`.FastxFile` performance. (PR #1227, thanks to Fabian Klötzl
   and Valentyn Bezshapkin)
 
-* Improve the accuracy of type hints for :class:`.AlignmentFile` iteration
+* Improve the accuracy of type hints for :class:`.AlignmentFile` iteration.
   (#1184, PR #1189, reported by @PikalaxALT)
 
 Documentation improvements:
 
-* Clarify that :meth:`.AlignedSegment.get_aligned_pairs` results are 0-based
+* Clarify that :meth:`.AlignedSegment.get_aligned_pairs` results are 0-based.
   (#1180, reported by Nick Semenkovich)
 
-* Clarify :meth:`.AlignedSegment.get_reference_positions` documentation
+* Clarify :meth:`.AlignedSegment.get_reference_positions` documentation.
   (#836, #838, reported by Liang Ou and Nick Stoler)
 
 * Clarify that installation via pip usually uses a wheel, and that configuring
   the build via $HTSLIB_CONFIGURE_OPTIONS etc only applies when installing from
-  an sdist (#1086, reported by Layne Sadler)
+  an sdist. (#1086, reported by Layne Sadler)
 
 A message from pysam's founder, Andreas Heger:
 
index b50e2e51bb95e4b351d94298e698b38ce5b7a9a0..89a51f3c266b3dfdd497e965e91a138fc5fea05c 100644 (file)
@@ -25,10 +25,10 @@ as it resolves non-python dependencies and uses pre-configured
 compilation options. Especially for OS X this will potentially save a
 lot of trouble.
 
-The current version of pysam wraps 3rd-party code from htslib-1.18, samtools-1.18, and bcftools-1.18.
+The current version of pysam wraps 3rd-party code from htslib-1.21, samtools-1.21, and bcftools-1.21.
 
-Pysam is available through `pypi
-<https://pypi.python.org/pypi/pysam>`_. To install, type::
+Pysam is available through `PyPI <https://pypi.org/project/pysam/>`_.
+To install, type::
 
    pip install pysam
 
index 3a6cab30a30bb583a741ff68c4785fe96b1117bf..75d3f8b49b8c69817c2c4e6055804ed7a7725ee8 100644 (file)
@@ -127,7 +127,7 @@ double *hmm_get_fwd_bwd_prob(hmm_t *hmm);
  *   @sites:    list of positions
  *
  *   Same as hmm_run_fwd_bwd, in addition a pointer to a matrix with the new
- *   transition probabilities is returned. In this verison, emission
+ *   transition probabilities is returned. In this version, emission
  *   probabilities are not updated.
  */
 double *hmm_run_baum_welch(hmm_t *hmm, int nsites, double *eprob, uint32_t *sites);
index 46dc0e0e31d8af12effea1b0c07955dcb3e1aa68..dbe9739eaabfb196620fbbe5b4b353b5ec451add 100644 (file)
@@ -9,7 +9,7 @@ the INSTALL document), the use of this software is governed by the GPL license.
 
 The MIT/Expat License
 
-Copyright (C) 2012-2023 Genome Research Ltd.
+Copyright (C) 2012-2024 Genome Research Ltd.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -772,3 +772,28 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-----------------------------------------------------------------------------
+
+License for edlib.[ch]
+
+The MIT License (MIT)
+
+Copyright (c) 2014 Martin Šošić
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
index 7958cf570634793fbf3563acbce719ebd0e4e430..b125679b9b7ab07aea78422d40a2436f66e4aa65 100644 (file)
@@ -1,6 +1,6 @@
 /* The MIT License
 
-   Copyright (c) 2021-2023 Genome Research Ltd.
+   Copyright (c) 2021-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -43,6 +43,7 @@ typedef struct
     kstring_t ref, alt;
     int ial;        // the index of the original ALT allele, 1-based
     int beg, end;   // 0-based inclusive offsets to ref,alt
+    int plen;       // the ref,alt prefix length, eg plen=1 for C>CA
 }
 atom_t;
 
@@ -175,8 +176,9 @@ static void _atomize_allele(abuf_t *buf, bcf1_t *rec, int ial)
                 atom->alt.l = 0;
                 kputc(refb, &atom->ref);
                 kputc(refb, &atom->alt);
-                atom->beg = atom->end = i;
-                atom->ial = ial;
+                atom->beg  = atom->end = i;
+                atom->ial  = ial;
+                atom->plen = 1;
             }
             continue;
         }
@@ -202,6 +204,35 @@ static int _atoms_inconsistent(const atom_t *a, const atom_t *b)
     if ( rcmp ) return rcmp;
     return strcasecmp(a->alt.s,b->alt.s);
 }
+
+// returns
+//      0 .. identical beg,ref,alt
+//      1 .. non-overlapping variants, but record may overlap (A>AT vs A>C)
+//      2 .. overlapping (conflicting) variants
+static int _atoms_overlap(const atom_t *a, const atom_t *b)
+{
+    if ( a->beg < b->beg ) return 2;
+    if ( a->beg > b->beg ) return 2;
+
+    // consider SNV followed by DEL as not overlapping
+    //      CC > C      a.plen=1 (ref,alt prefix len=1)
+    //      C  > T      b.plen=0 (ref,alt prefix len=0)
+    if ( a->plen && a->plen >= b->ref.l ) return 1;
+    if ( b->plen && b->plen >= a->ref.l ) return 1;
+
+    int rcmp = strcasecmp(a->ref.s,b->ref.s);
+    if ( rcmp ) return 2;
+
+    // consider SNV followed by INS as not overlapping
+    //      A > AT      a.plen=1 (ref,alt prefix len=1)
+    //      A > C       b.plen=0 (ref,alt prefix len=0)
+    if ( a->plen && a->plen >= b->alt.l ) return 1;
+    if ( b->plen && b->plen >= a->alt.l ) return 1;
+
+    rcmp = strcasecmp(a->alt.s,b->alt.s);
+    if ( rcmp ) return 2;
+    return 0;
+}
 /*
     For reproducibility of tests on different platforms, we need to guarantee the same order of identical
     atoms originating from different source ALTs.  Even though they are consistent, different values can be
@@ -238,7 +269,14 @@ static void _split_table_new(abuf_t *buf, atom_t *atom)
 static void _split_table_overlap(abuf_t *buf, int iout, atom_t *atom)
 {
     uint8_t *ptr = buf->split.tbl + iout*buf->split.nori;
-    ptr[atom->ial-1] = _atoms_inconsistent(atom,buf->split.atoms[iout]) ? 2 : 1;
+    int olap = _atoms_overlap(atom,buf->split.atoms[iout]);
+    ptr[atom->ial-1] =  olap > 1 ? 2 : 1;
+
+    // The test test/atomize.split.5.vcf shows why we sometimes can and sometimes
+    // cannot remove the star allele like this
+    //      buf->split.overlaps[iout] = olap > 1 ? 1 : 0;
+    // I forgot the details of the code, so don't immediately see
+    // if this could be made smarter
     buf->split.overlaps[iout] = 1;
 }
 #if 0
@@ -411,13 +449,21 @@ static void _split_table_set_info(abuf_t *buf, bcf_info_t *info, merge_rule_t mo
             buf->tmp2  = dst.s;
             ret = bcf_update_info(buf->out_hdr, out, tag, buf->tmp2, dst.l, type);
         }
-        if ( ret!=0 ) error("An error occurred while updating INFO/%s\n",tag);
+        if ( ret!=0 ) error("An error occurred while updating INFO/%s (errcode=%d)\n",tag,ret);
     }
 }
 static void _split_table_set_history(abuf_t *buf)
 {
-    int i,j;
+    int i,j,ret;
     bcf1_t *rec = buf->split.rec;
+
+    // Don't update if the tag already exists. This is to prevent -a from overwriting -m
+    int m = 0;
+    char *tmp = NULL;
+    ret = bcf_get_info_string(buf->hdr,rec,buf->split.info_tag,&tmp,&m);
+    free(tmp);
+    if ( ret>0 ) return;
+
     buf->tmps.l = 0;
     ksprintf(&buf->tmps,"%s|%"PRIhts_pos"|%s|",bcf_seqname(buf->hdr,rec),rec->pos+1,rec->d.allele[0]);
     for (i=1; i<rec->n_allele; i++)
@@ -441,8 +487,8 @@ static void _split_table_set_history(abuf_t *buf)
             kputc(',',&buf->tmps);
         }
         buf->tmps.s[--buf->tmps.l] = 0;
-        if ( (bcf_update_info_string(buf->out_hdr, out, buf->split.info_tag, buf->tmps.s))!=0 )
-            error("An error occurred while updating INFO/%s\n",buf->split.info_tag);
+        if ( (ret=bcf_update_info_string(buf->out_hdr, out, buf->split.info_tag, buf->tmps.s))!=0 )
+            error("An error occurred while updating INFO/%s (errcode=%d)\n",buf->split.info_tag,ret);
     }
 }
 static void _split_table_set_gt(abuf_t *buf)
@@ -668,7 +714,7 @@ static void _split_table_set_format(abuf_t *buf, bcf_fmt_t *fmt, merge_rule_t mo
             #undef BRANCH
             ret = bcf_update_format(buf->out_hdr, out, tag, buf->tmp2, 3*(1+star_allele)*nsmpl, type);
         }
-        if ( ret!=0 ) error("An error occurred while updating FORMAT/%s\n",tag);
+        if ( ret!=0 ) error("An error occurred while updating FORMAT/%s (errcode=%d)\n",tag,ret);
     }
 }
 static inline int _is_acgtn(char *seq)
@@ -737,7 +783,7 @@ void _abuf_split(abuf_t *buf, bcf1_t *rec)
     _split_table_init(buf,rec,buf->natoms);
     for (i=0; i<buf->natoms; i++)
     {
-        if ( i && !_atoms_inconsistent(&buf->atoms[i-1],&buf->atoms[i]) ) continue;
+        if ( i && _atoms_inconsistent(&buf->atoms[i-1],&buf->atoms[i])==0 ) continue;
         _split_table_new(buf, &buf->atoms[i]);  // add a new unique output atom
     }
     for (i=0; i<buf->natoms; i++)
index 6ac6d183274e847237da5ae29d5317994467f95d..6e0e3de5c5ea0df271b2f069ee0830a0c599976f 100644 (file)
@@ -2,7 +2,7 @@
 
 /* The MIT License
 
-   Copyright (c) 2021-2023 Genome Research Ltd.
+   Copyright (c) 2021-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -45,6 +45,7 @@ typedef struct
     kstring_t ref, alt;
     int ial;        // the index of the original ALT allele, 1-based
     int beg, end;   // 0-based inclusive offsets to ref,alt
+    int plen;       // the ref,alt prefix length, eg plen=1 for C>CA
 }
 atom_t;
 
@@ -177,8 +178,9 @@ static void _atomize_allele(abuf_t *buf, bcf1_t *rec, int ial)
                 atom->alt.l = 0;
                 kputc(refb, &atom->ref);
                 kputc(refb, &atom->alt);
-                atom->beg = atom->end = i;
-                atom->ial = ial;
+                atom->beg  = atom->end = i;
+                atom->ial  = ial;
+                atom->plen = 1;
             }
             continue;
         }
@@ -204,6 +206,35 @@ static int _atoms_inconsistent(const atom_t *a, const atom_t *b)
     if ( rcmp ) return rcmp;
     return strcasecmp(a->alt.s,b->alt.s);
 }
+
+// returns
+//      0 .. identical beg,ref,alt
+//      1 .. non-overlapping variants, but record may overlap (A>AT vs A>C)
+//      2 .. overlapping (conflicting) variants
+static int _atoms_overlap(const atom_t *a, const atom_t *b)
+{
+    if ( a->beg < b->beg ) return 2;
+    if ( a->beg > b->beg ) return 2;
+
+    // consider SNV followed by DEL as not overlapping
+    //      CC > C      a.plen=1 (ref,alt prefix len=1)
+    //      C  > T      b.plen=0 (ref,alt prefix len=0)
+    if ( a->plen && a->plen >= b->ref.l ) return 1;
+    if ( b->plen && b->plen >= a->ref.l ) return 1;
+
+    int rcmp = strcasecmp(a->ref.s,b->ref.s);
+    if ( rcmp ) return 2;
+
+    // consider SNV followed by INS as not overlapping
+    //      A > AT      a.plen=1 (ref,alt prefix len=1)
+    //      A > C       b.plen=0 (ref,alt prefix len=0)
+    if ( a->plen && a->plen >= b->alt.l ) return 1;
+    if ( b->plen && b->plen >= a->alt.l ) return 1;
+
+    rcmp = strcasecmp(a->alt.s,b->alt.s);
+    if ( rcmp ) return 2;
+    return 0;
+}
 /*
     For reproducibility of tests on different platforms, we need to guarantee the same order of identical
     atoms originating from different source ALTs.  Even though they are consistent, different values can be
@@ -240,7 +271,14 @@ static void _split_table_new(abuf_t *buf, atom_t *atom)
 static void _split_table_overlap(abuf_t *buf, int iout, atom_t *atom)
 {
     uint8_t *ptr = buf->split.tbl + iout*buf->split.nori;
-    ptr[atom->ial-1] = _atoms_inconsistent(atom,buf->split.atoms[iout]) ? 2 : 1;
+    int olap = _atoms_overlap(atom,buf->split.atoms[iout]);
+    ptr[atom->ial-1] =  olap > 1 ? 2 : 1;
+
+    // The test test/atomize.split.5.vcf shows why we sometimes can and sometimes
+    // cannot remove the star allele like this
+    //      buf->split.overlaps[iout] = olap > 1 ? 1 : 0;
+    // I forgot the details of the code, so don't immediately see
+    // if this could be made smarter
     buf->split.overlaps[iout] = 1;
 }
 #if 0
@@ -413,13 +451,21 @@ static void _split_table_set_info(abuf_t *buf, bcf_info_t *info, merge_rule_t mo
             buf->tmp2  = dst.s;
             ret = bcf_update_info(buf->out_hdr, out, tag, buf->tmp2, dst.l, type);
         }
-        if ( ret!=0 ) error("An error occurred while updating INFO/%s\n",tag);
+        if ( ret!=0 ) error("An error occurred while updating INFO/%s (errcode=%d)\n",tag,ret);
     }
 }
 static void _split_table_set_history(abuf_t *buf)
 {
-    int i,j;
+    int i,j,ret;
     bcf1_t *rec = buf->split.rec;
+
+    // Don't update if the tag already exists. This is to prevent -a from overwriting -m
+    int m = 0;
+    char *tmp = NULL;
+    ret = bcf_get_info_string(buf->hdr,rec,buf->split.info_tag,&tmp,&m);
+    free(tmp);
+    if ( ret>0 ) return;
+
     buf->tmps.l = 0;
     ksprintf(&buf->tmps,"%s|%"PRIhts_pos"|%s|",bcf_seqname(buf->hdr,rec),rec->pos+1,rec->d.allele[0]);
     for (i=1; i<rec->n_allele; i++)
@@ -443,8 +489,8 @@ static void _split_table_set_history(abuf_t *buf)
             kputc(',',&buf->tmps);
         }
         buf->tmps.s[--buf->tmps.l] = 0;
-        if ( (bcf_update_info_string(buf->out_hdr, out, buf->split.info_tag, buf->tmps.s))!=0 )
-            error("An error occurred while updating INFO/%s\n",buf->split.info_tag);
+        if ( (ret=bcf_update_info_string(buf->out_hdr, out, buf->split.info_tag, buf->tmps.s))!=0 )
+            error("An error occurred while updating INFO/%s (errcode=%d)\n",buf->split.info_tag,ret);
     }
 }
 static void _split_table_set_gt(abuf_t *buf)
@@ -670,7 +716,7 @@ static void _split_table_set_format(abuf_t *buf, bcf_fmt_t *fmt, merge_rule_t mo
             #undef BRANCH
             ret = bcf_update_format(buf->out_hdr, out, tag, buf->tmp2, 3*(1+star_allele)*nsmpl, type);
         }
-        if ( ret!=0 ) error("An error occurred while updating FORMAT/%s\n",tag);
+        if ( ret!=0 ) error("An error occurred while updating FORMAT/%s (errcode=%d)\n",tag,ret);
     }
 }
 static inline int _is_acgtn(char *seq)
@@ -739,7 +785,7 @@ void _abuf_split(abuf_t *buf, bcf1_t *rec)
     _split_table_init(buf,rec,buf->natoms);
     for (i=0; i<buf->natoms; i++)
     {
-        if ( i && !_atoms_inconsistent(&buf->atoms[i-1],&buf->atoms[i]) ) continue;
+        if ( i && _atoms_inconsistent(&buf->atoms[i-1],&buf->atoms[i])==0 ) continue;
         _split_table_new(buf, &buf->atoms[i]);  // add a new unique output atom
     }
     for (i=0; i<buf->natoms; i++)
index 88e25de1f2bd3abec3ade5750ccd1f9dde7a8ac2..55c20812200407c8e540c7eff1deacd8fe80a0be 100644 (file)
@@ -1,7 +1,7 @@
 /*  bam2bcf.c -- variant calling.
 
     Copyright (C) 2010-2012 Broad Institute.
-    Copyright (C) 2012-2022 Genome Research Ltd.
+    Copyright (C) 2012-2024 Genome Research Ltd.
 
     Author: Heng Li <lh3@sanger.ac.uk>
 
@@ -249,6 +249,10 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
 {
     int i, n, ref4, is_indel, ori_depth = 0;
 
+#ifdef GLF_DEBUG
+    fprintf(stderr, "Call GLFGEN\n");
+#endif
+
     // clean from previous run
     r->ori_depth = 0;
     r->mq0 = 0;
@@ -268,6 +272,15 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
         bca->bases = (uint16_t*)realloc(bca->bases, 2 * bca->max_bases);
     }
 
+    // Detect if indel occurs anywhere in this sample
+    int indel_in_sample = 0;
+    if (bca->edlib) {
+        for (i = n = 0; i < _n; ++i) {
+            const bam_pileup1_t *p = pl + i;
+            if (p->indel) indel_in_sample = 1;
+        }
+    }
+
     // fill the bases array
     double nqual_over_60 = bca->nqual / 60.0;
     int ADR_ref_missed[4] = {0};
@@ -298,7 +311,19 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
             b = p->aux>>16&0x3f;        // indel type
             seqQ = q = (p->aux & 0xff); // mp2 + builtin indel-bias
 
-            if ( !bca->indels_v20 )
+            if (bca->edlib) {
+                if (indel_in_sample) {
+                    seqQ = q = (p->aux & 0xff); // mp2 + builtin indel-bias
+                } else if (p->aux & 0xff) {
+                    // An indel in another sample, but not this.  So just use
+                    // basic sequence confidences.
+                    q = bam_get_qual(p->b)[p->qpos];
+                    if (q > bca->max_baseQ) q = bca->max_baseQ;
+                    seqQ = 99;
+                }
+            }
+
+            if ( !bca->indels_v20 && !bca->edlib )
             {
                 /*
                     This heuristics was introduced by e4e161068 and claims to fix #1446. However, we obtain
@@ -330,6 +355,10 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
                 }
             }
 
+#ifdef GLF_DEBUG
+            fprintf(stderr, "GLF %s\t%d\t%d\n", bam_get_qname(p->b),
+                    bca->indel_types[b], q);
+#endif
             if (q < bca->min_baseQ)
             {
                 if (!p->indel && b < 4) // not an indel read
@@ -341,6 +370,50 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
                 }
                 continue;
             }
+
+#ifndef MIN
+#define MIN(a,b) ((a)<(b)?(a):(b))
+#endif
+
+#ifndef MAX
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#endif
+
+#if 1 // TEST 6
+            if (bca->edlib) {
+                // Deeper data should rely more heavily on counts of data
+                // than quality, as quality can be unreliable and prone to
+                // miscalculations through BAQ, STR analysis, etc.
+                // So we put a cap on how good seqQ can be.
+                //
+                // Is it simply the equivalent of increasing -F filter?
+                // Not quite, as the latter removes many real variants upfront.
+                // This calls them and then post-adjusts quality, potentially
+                // dropping it later or changing genotype. So we still get
+                // calls, but lower qual.
+                seqQ = MIN(seqQ, bca->seqQ_offset-(MIN(20,_n)*5));
+
+                if (indel_in_sample && p->indel == 0 && b != 0) {
+                    // This read doesn't contain an indel in CIGAR, but it
+                    // is assigned to an indel now (b != 0),  These are
+                    // reads we've corrected with realignment, but they're
+                    // also enriched for FPs so at high depth we reduce their
+                    // confidence and let the depth do the talking.  If it's
+                    // real and deep, then we don't need every read aligning.
+                    // We also reduce base quality too to reflect the
+                    // chance of our realignment being incorrect.
+
+                    seqQ = MIN(seqQ, seqQ/2 + 5); // q2p5
+
+                    // Finally reduce indel quality.
+                    // This is a blend of indelQ and base QUAL.
+                    q = MIN((int)bam_get_qual(p->b)[p->qpos]/4+10, q/4+1);
+                }
+            }
+#endif
+
+            // Note baseQ changes some output fields such as I16, but has no
+            // significant affect on "call".
             baseQ  = p->aux>>8&0xff;
         }
         else
@@ -375,6 +448,11 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
         }
         mapQ  = p->b->core.qual < 255? p->b->core.qual : DEF_MAPQ; // special case for mapQ==255
         if ( !mapQ ) r->mq0++;
+#ifdef GLF_DEBUG
+        fprintf(stderr, "GLF2 %s\t%d\t%d\t%d,%d\n",
+                bam_get_qname(p->b), b, q,
+                seqQ, mapQ);
+#endif
         if (q > seqQ) q = seqQ;
         mapQ = mapQ < bca->capQ? mapQ : bca->capQ;
         if (q > mapQ) q = mapQ;
@@ -478,9 +556,19 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
             for (i=0; i<4; i++) r->ADF[i] += lroundf((float)dp_ambig * r->ADF[i]/dp);
     }
 
+    // Else consider downgrading bca->bases[] scores by AD vs AD_ref_missed
+    // ratios.  This is detrimental on Illumina, but beneficial on PacBio CCS.
+    // It's possibly related to the homopolyer error likelihoods or overall
+    // Indel accuracy.  Maybe tie this in to the -h option?
+
     r->ori_depth = ori_depth;
     // glfgen
     errmod_cal(bca->e, n, 5, bca->bases, r->p); // calculate PL of each genotype
+
+    // TODO: account for the number of unassigned reads.  If depth is 50,
+    // but AD is 5,7 then it may look like a variant but it probably
+    // should be low quality.
+
     return n;
 }
 
@@ -1147,10 +1235,30 @@ int bcf_call2bcf(bcf_call_t *bc, bcf1_t *rec, bcf_callret1_t *bcr, int fmt_flag,
     if ( bc->ori_ref < 0 )
     {
         bcf_update_info_flag(hdr, rec, "INDEL", NULL, 1);
-        if ( fmt_flag&B2B_INFO_IDV )
-            bcf_update_info_int32(hdr, rec, "IDV", &bca->max_support, 1);
-        if ( fmt_flag&B2B_INFO_IMF )
-            bcf_update_info_float(hdr, rec, "IMF", &bca->max_frac, 1);
+        uint32_t idv = bca->max_support;
+        if ( fmt_flag&B2B_INFO_IMF) {
+            float max_frac;
+            // Recompute IDV and IMF based on alignment results for more
+            // accurate counts, but only when in new "--indels-cns" mode.
+            if (bc->ADF && bc->ADR && bca->edlib) {
+                int max_ad = 0;
+                for (int k = 1; k < rec->n_allele; k++) {
+                    if (max_ad < bc->ADF[k] + bc->ADR[k])
+                        max_ad = bc->ADF[k] + bc->ADR[k];
+                }
+                max_frac = (double)(max_ad) / bc->ori_depth;
+                idv = max_ad;
+            } else {
+                max_frac = bca->max_frac;
+            }
+            // Copied here to maintain order for consistency of "make check"
+            if ( fmt_flag&B2B_INFO_IDV )
+                bcf_update_info_int32(hdr, rec, "IDV", &idv, 1);
+            bcf_update_info_float(hdr, rec, "IMF", &max_frac, 1);
+        } else {
+            if ( fmt_flag&B2B_INFO_IDV )
+                bcf_update_info_int32(hdr, rec, "IDV", &idv, 1);
+        }
     }
     bcf_update_info_int32(hdr, rec, "DP", &bc->ori_depth, 1);
     if ( fmt_flag&B2B_INFO_ADF )
index 4a6fe4d0e1881f3791e4783c33c198cfc2610645..d0941b2eb5368ea9e419efa6664a583e662a61a7 100644 (file)
@@ -3,7 +3,7 @@
 /*  bam2bcf.c -- variant calling.
 
     Copyright (C) 2010-2012 Broad Institute.
-    Copyright (C) 2012-2022 Genome Research Ltd.
+    Copyright (C) 2012-2024 Genome Research Ltd.
 
     Author: Heng Li <lh3@sanger.ac.uk>
 
@@ -251,6 +251,10 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
 {
     int i, n, ref4, is_indel, ori_depth = 0;
 
+#ifdef GLF_DEBUG
+    fprintf(bcftools_stderr, "Call GLFGEN\n");
+#endif
+
     // clean from previous run
     r->ori_depth = 0;
     r->mq0 = 0;
@@ -270,6 +274,15 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
         bca->bases = (uint16_t*)realloc(bca->bases, 2 * bca->max_bases);
     }
 
+    // Detect if indel occurs anywhere in this sample
+    int indel_in_sample = 0;
+    if (bca->edlib) {
+        for (i = n = 0; i < _n; ++i) {
+            const bam_pileup1_t *p = pl + i;
+            if (p->indel) indel_in_sample = 1;
+        }
+    }
+
     // fill the bases array
     double nqual_over_60 = bca->nqual / 60.0;
     int ADR_ref_missed[4] = {0};
@@ -300,7 +313,19 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
             b = p->aux>>16&0x3f;        // indel type
             seqQ = q = (p->aux & 0xff); // mp2 + builtin indel-bias
 
-            if ( !bca->indels_v20 )
+            if (bca->edlib) {
+                if (indel_in_sample) {
+                    seqQ = q = (p->aux & 0xff); // mp2 + builtin indel-bias
+                } else if (p->aux & 0xff) {
+                    // An indel in another sample, but not this.  So just use
+                    // basic sequence confidences.
+                    q = bam_get_qual(p->b)[p->qpos];
+                    if (q > bca->max_baseQ) q = bca->max_baseQ;
+                    seqQ = 99;
+                }
+            }
+
+            if ( !bca->indels_v20 && !bca->edlib )
             {
                 /*
                     This heuristics was introduced by e4e161068 and claims to fix #1446. However, we obtain
@@ -332,6 +357,10 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
                 }
             }
 
+#ifdef GLF_DEBUG
+            fprintf(bcftools_stderr, "GLF %s\t%d\t%d\n", bam_get_qname(p->b),
+                    bca->indel_types[b], q);
+#endif
             if (q < bca->min_baseQ)
             {
                 if (!p->indel && b < 4) // not an indel read
@@ -343,6 +372,50 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
                 }
                 continue;
             }
+
+#ifndef MIN
+#define MIN(a,b) ((a)<(b)?(a):(b))
+#endif
+
+#ifndef MAX
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#endif
+
+#if 1 // TEST 6
+            if (bca->edlib) {
+                // Deeper data should rely more heavily on counts of data
+                // than quality, as quality can be unreliable and prone to
+                // miscalculations through BAQ, STR analysis, etc.
+                // So we put a cap on how good seqQ can be.
+                //
+                // Is it simply the equivalent of increasing -F filter?
+                // Not quite, as the latter removes many real variants upfront.
+                // This calls them and then post-adjusts quality, potentially
+                // dropping it later or changing genotype. So we still get
+                // calls, but lower qual.
+                seqQ = MIN(seqQ, bca->seqQ_offset-(MIN(20,_n)*5));
+
+                if (indel_in_sample && p->indel == 0 && b != 0) {
+                    // This read doesn't contain an indel in CIGAR, but it
+                    // is assigned to an indel now (b != 0),  These are
+                    // reads we've corrected with realignment, but they're
+                    // also enriched for FPs so at high depth we reduce their
+                    // confidence and let the depth do the talking.  If it's
+                    // real and deep, then we don't need every read aligning.
+                    // We also reduce base quality too to reflect the
+                    // chance of our realignment being incorrect.
+
+                    seqQ = MIN(seqQ, seqQ/2 + 5); // q2p5
+
+                    // Finally reduce indel quality.
+                    // This is a blend of indelQ and base QUAL.
+                    q = MIN((int)bam_get_qual(p->b)[p->qpos]/4+10, q/4+1);
+                }
+            }
+#endif
+
+            // Note baseQ changes some output fields such as I16, but has no
+            // significant affect on "call".
             baseQ  = p->aux>>8&0xff;
         }
         else
@@ -377,6 +450,11 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
         }
         mapQ  = p->b->core.qual < 255? p->b->core.qual : DEF_MAPQ; // special case for mapQ==255
         if ( !mapQ ) r->mq0++;
+#ifdef GLF_DEBUG
+        fprintf(bcftools_stderr, "GLF2 %s\t%d\t%d\t%d,%d\n",
+                bam_get_qname(p->b), b, q,
+                seqQ, mapQ);
+#endif
         if (q > seqQ) q = seqQ;
         mapQ = mapQ < bca->capQ? mapQ : bca->capQ;
         if (q > mapQ) q = mapQ;
@@ -480,9 +558,19 @@ int bcf_call_glfgen(int _n, const bam_pileup1_t *pl, int ref_base, bcf_callaux_t
             for (i=0; i<4; i++) r->ADF[i] += lroundf((float)dp_ambig * r->ADF[i]/dp);
     }
 
+    // Else consider downgrading bca->bases[] scores by AD vs AD_ref_missed
+    // ratios.  This is detrimental on Illumina, but beneficial on PacBio CCS.
+    // It's possibly related to the homopolyer error likelihoods or overall
+    // Indel accuracy.  Maybe tie this in to the -h option?
+
     r->ori_depth = ori_depth;
     // glfgen
     errmod_cal(bca->e, n, 5, bca->bases, r->p); // calculate PL of each genotype
+
+    // TODO: account for the number of unassigned reads.  If depth is 50,
+    // but AD is 5,7 then it may look like a variant but it probably
+    // should be low quality.
+
     return n;
 }
 
@@ -1149,10 +1237,30 @@ int bcf_call2bcf(bcf_call_t *bc, bcf1_t *rec, bcf_callret1_t *bcr, int fmt_flag,
     if ( bc->ori_ref < 0 )
     {
         bcf_update_info_flag(hdr, rec, "INDEL", NULL, 1);
-        if ( fmt_flag&B2B_INFO_IDV )
-            bcf_update_info_int32(hdr, rec, "IDV", &bca->max_support, 1);
-        if ( fmt_flag&B2B_INFO_IMF )
-            bcf_update_info_float(hdr, rec, "IMF", &bca->max_frac, 1);
+        uint32_t idv = bca->max_support;
+        if ( fmt_flag&B2B_INFO_IMF) {
+            float max_frac;
+            // Recompute IDV and IMF based on alignment results for more
+            // accurate counts, but only when in new "--indels-cns" mode.
+            if (bc->ADF && bc->ADR && bca->edlib) {
+                int max_ad = 0;
+                for (int k = 1; k < rec->n_allele; k++) {
+                    if (max_ad < bc->ADF[k] + bc->ADR[k])
+                        max_ad = bc->ADF[k] + bc->ADR[k];
+                }
+                max_frac = (double)(max_ad) / bc->ori_depth;
+                idv = max_ad;
+            } else {
+                max_frac = bca->max_frac;
+            }
+            // Copied here to maintain order for consistency of "make check"
+            if ( fmt_flag&B2B_INFO_IDV )
+                bcf_update_info_int32(hdr, rec, "IDV", &idv, 1);
+            bcf_update_info_float(hdr, rec, "IMF", &max_frac, 1);
+        } else {
+            if ( fmt_flag&B2B_INFO_IDV )
+                bcf_update_info_int32(hdr, rec, "IDV", &idv, 1);
+        }
     }
     bcf_update_info_int32(hdr, rec, "DP", &bc->ori_depth, 1);
     if ( fmt_flag&B2B_INFO_ADF )
index 955c022bf3c9f3e243b2026bf076074aeff9f54f..8f8f8db5a21f4b987f4d2f8dd107cd5076188912 100644 (file)
@@ -122,14 +122,17 @@ typedef struct __bcf_callaux_t {
     // for internal uses
     int max_bases;
     int indel_types[4];     // indel lengths
-    int indel_win_size, indels_v20;
-    int maxins, indelreg;
+    int indel_win_size, indels_v20, edlib;
+    int seqQ_offset; // edlib mode, seqQ=MIN(seqQ, offset - MIN(20,depth)*5);
+    int maxins, indelreg, poly_mqual;
     int read_len;
     char *inscns;
     uint16_t *bases;        // 5bit: unused, 6:quality, 1:is_rev, 4:2-bit base or indel allele (index to bcf_callaux_t.indel_types)
     errmod_t *e;
     void *rghash;
     float indel_bias;  // adjusts indel score threshold; lower => call more.
+    float del_bias;    // (-.9 < x < .9) error profile; >0 => more del, <0 => more ins
+    float vs_ref;      // 0 to 1.  0: score vs next-best. 1: score vs ref
     int32_t *ref_nm, *alt_nm;   // pointers to bcf_call_t.{ref_nm,alt_nm}
     unsigned int nnm[2];        // number of nm observations
     float nm[2];                // cumulative count of mismatches in ref and alt reads
@@ -193,11 +196,35 @@ extern "C" {
                      const bcf_callaux_t *bca, const char *ref);
     int bcf_call_gap_prep(int n, int *n_plp, bam_pileup1_t **plp, int pos, bcf_callaux_t *bca, const char *ref);
     int bcf_iaux_gap_prep(int n, int *n_plp, bam_pileup1_t **plp, int pos, bcf_callaux_t *bca, const char *ref);
+    int bcf_edlib_gap_prep(int n, int *n_plp, bam_pileup1_t **plp, int pos,
+                           bcf_callaux_t *bca, const char *ref, int ref_len);
+
     void bcf_callaux_clean(bcf_callaux_t *bca, bcf_call_t *call);
 
     int bcf_cgp_l_run(const char *ref, int pos);
     int est_indelreg(int pos, const char *ref, int l, char *ins4);
 
+/* ----------------------------------------------------------------------
+ * Shared between bam2bcf_indel.c and bam2bcf_edlib.c
+ */
+
+// Take a reference position tpos and convert to a query position (returned).
+// This uses the CIGAR string plus alignment c->pos to do the mapping.
+//
+// *_tpos is returned as tpos if query overlaps tpos, but for deletions
+// it'll be either the start (is_left) or end (!is_left) ref position.
+int tpos2qpos(const bam1_core_t *c, const uint32_t *cigar, int32_t tpos, int is_left, int32_t *_tpos);
+
+// Identify spft-clip length, position in seq, and clipped seq len
+void get_pos(const bcf_callaux_t *bca, bam_pileup1_t *p,
+             int *sc_len_r, int *slen_r, int *epos_r, int *end);
+
+// Compute the consensus for this sample 's', minus indels which
+// get added later.
+char *bcf_cgp_calc_cons(int n, int *n_plp, bam_pileup1_t **plp,
+                        int pos, int *types, int n_types,
+                        int max_ins, int s);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/bcftools/bam2bcf_edlib.c b/bcftools/bam2bcf_edlib.c
new file mode 100644 (file)
index 0000000..4e0a38c
--- /dev/null
@@ -0,0 +1,1704 @@
+/*  bam2bcf_indel.c -- indel caller.
+
+    Copyright (C) 2010, 2011 Broad Institute.
+    Copyright (C) 2012-2014,2016-2017, 2021-2024 Genome Research Ltd.
+
+    Author: Heng Li <lh3@sanger.ac.uk>
+            Petr Danecek <pd3@sanger.ac.uk>
+           James Bonfield <jkb@sanger.ac.uk>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.  */
+
+// Show consensus
+//#define CONS_DEBUG
+
+// Show alignments to consensus
+//#define ALIGN_DEBUG
+
+#include <assert.h>
+#include <ctype.h>
+#include <string.h>
+#include <math.h>
+#include <htslib/hts.h>
+#include <htslib/sam.h>
+#include <htslib/khash_str2int.h>
+#include "bam2bcf.h"
+#include "str_finder.h"
+
+#include <htslib/ksort.h>
+// Is there no way to share these between the 3 implementations?
+KSORT_INIT_STATIC_GENERIC(uint32_t)
+
+#define MINUS_CONST 0x10000000
+
+#define MAX_TYPES 64
+
+#ifndef MIN
+#  define MIN(a,b) ((a)<(b)?(a):(b))
+#endif
+
+#ifndef ABS
+#  define ABS(a) ((a)<0?-(a):(a))
+#endif
+
+#ifndef MAX
+#  define MAX(a,b) ((a)>(b)?(a):(b))
+#endif
+
+// l is the relative gap length and l_run is the length of the homopolymer
+// on the reference.
+//
+// Larger seqQ is good, so increasing tandemQ calls more indels,
+// and longer l_run means fewer calls.  It is capped later at 255.
+// For short l_runs, the qual is simply based on size of indel
+// larger ones being considered more likely to be real.
+// Longer indels get assigned a score based on the relative indel size
+// to homopolymer, where l_run base will have already been verified by
+// the caller to ensure it's compatible.
+static inline int est_seqQ(const bcf_callaux_t *bca, int l, int l_run, int str_len)
+{
+    int q, qh;
+    // Short indels are more likely sequencing error than large ones.
+    // So "seqQ" scales with size of observation "l".
+    //
+    // Note openQ and extQ are error likelihoods in Phred scale.  Hence high
+    // openQ means we're very unlikely to miscall an indel.
+    // Ie it's not the open/ext "costs" normally used in alignment; more the reverse.
+    //
+    // We use MIN(q,qh) below, so we can remove the q component by specifying
+    // a large -o parameter in mpileup.
+    q = bca->openQ + bca->extQ * (abs(l) - 1);
+
+    // Orig method; best with Illumina (high openQ)
+//    qh = bca->tandemQ * (double)abs(l) / l_run + .499;
+
+    // Penalise longer homopolymers quadratically more, but boost shorter ones.
+    // Best with CCS (low openQ)
+    //qh = 2 * bca->tandemQ * pow((double)abs(l) / l_run, 1.5) + .499;
+
+    // (l/l_run)^1.26 for openQ=25 or ^1 for openQ=40.
+//    double openQ = MIN(40, bca->openQ);
+//    qh = (30/openQ) * bca->tandemQ
+//        * pow((double)abs(l) / l_run, 1/sqrt(openQ/40)) + .499;
+
+    // Linear scaled on openQ too
+    qh = bca->tandemQ * (double)abs(l) / l_run + .499;
+
+    // Generic maybe ?
+    // power = 1/sqrt(MIN(40,bca->openQ)/40.);
+    // qh = ... * pow((double)abs(l)/l_run, power)
+
+    // bam2bcf.c caps has "if q>seqQ) q=seqQ" so it caps base qual 'q'.
+    // A 1bp indel would therefore have a maximum qual it could be considered based
+    // on open+ext.  Hence why openQ is phred score indicating if the base is real
+    // or an over/under-call. (high openQ means high trust in base)
+    return q < qh? q : qh;
+}
+
+// Part of bcf_call_gap_prep.
+//
+// Scans the pileup to identify all the different sizes of indels
+// present.
+// types[] returned is sorted by size, from smallest (maybe negative) to largest.
+//
+// Returns types and fills out n_types_r,  max_rd_len_r and ref_type_r,
+//         or NULL on error.
+static int *bcf_cgp_find_types(int n, int *n_plp, bam_pileup1_t **plp,
+                               int pos, bcf_callaux_t *bca, const char *ref,
+                               int *max_rd_len_r, int *n_types_r,
+                               int *ref_type_r, int *N_r) {
+    int i, j, t, s, N, m, max_rd_len, n_types;
+    int n_alt = 0, n_tot = 0, indel_support_ok = 0;
+    uint32_t *aux;
+    int *types;
+
+    // N is the total number of reads
+    for (s = N = 0; s < n; ++s)
+        N += n_plp[s];
+
+    bca->max_support = bca->max_frac = 0;
+    aux = (uint32_t*) calloc(N + 1, 4);
+    if (!aux)
+        return NULL;
+
+    m = max_rd_len = 0;
+    aux[m++] = MINUS_CONST; // zero indel is always a type (REF)
+
+    // Fill out aux[] array with all the non-zero indel sizes.
+    // Also tally number with indels (n_alt) and total (n_tot).
+    for (s = 0; s < n; ++s) {
+        int na = 0, nt = 0;
+        for (i = 0; i < n_plp[s]; ++i) {
+            const bam_pileup1_t *p = plp[s] + i;
+            ++nt;
+            if (p->indel != 0) {
+                ++na;
+                aux[m++] = MINUS_CONST + p->indel;
+            }
+
+            // FIXME: cache me in pileup struct.
+            j = bam_cigar2qlen(p->b->core.n_cigar, bam_get_cigar(p->b));
+            if (j > max_rd_len) max_rd_len = j;
+        }
+        double frac = (double)na/nt;
+        if ( !indel_support_ok && na >= bca->min_support
+             && frac >= bca->min_frac )
+            indel_support_ok = 1;
+        if ( na > bca->max_support && frac > 0 )
+            bca->max_support = na, bca->max_frac = frac;
+
+        n_alt += na;
+        n_tot += nt;
+    }
+
+    // Sort aux[] and dedup
+    ks_introsort(uint32_t, m, aux);
+    for (i = 1, n_types = 1; i < m; ++i)
+        if (aux[i] != aux[i-1]) ++n_types;
+
+    // Taking totals makes it hard to call rare indels (IMF filter)
+    if ( !bca->per_sample_flt )
+        indel_support_ok = ( (double)n_alt / n_tot < bca->min_frac
+                             || n_alt < bca->min_support )
+            ? 0 : 1;
+    if ( n_types == 1 || !indel_support_ok ) { // then skip
+        free(aux);
+        return NULL;
+    }
+
+    // Bail out if we have far too many types of indel
+    if (n_types >= MAX_TYPES) {
+        free(aux);
+        // TODO revisit how/whether to control printing this warning
+        if (hts_verbose >= 2)
+            fprintf(stderr, "[%s] excessive INDEL alleles at position %d. "
+                    "Skip the position.\n", __func__, pos + 1);
+        return NULL;
+    }
+
+    // To prevent long stretches of N's to be mistaken for indels
+    // (sometimes thousands of bases), check the number of N's in the
+    // sequence and skip places where half or more reference bases are Ns.
+    int nN=0, i_end = pos + (2*bca->indel_win_size < max_rd_len
+                            ?2*bca->indel_win_size : max_rd_len);
+    for (i=pos; i<i_end && ref[i]; i++)
+        nN += ref[i] == 'N';
+    if ( nN*2>(i-pos) ) {
+        free(aux);
+        return NULL;
+    }
+
+    // Finally fill out the types[] array detailing the size of insertion
+    // or deletion.
+    types = (int*)calloc(n_types, sizeof(int));
+    if (!types) {
+        free(aux);
+        return NULL;
+    }
+    t = 0;
+    for (i = 0; i < m; ++i) {
+        int sz = (int32_t)(aux[i] - MINUS_CONST);
+        int j;
+        for (j = i+1; j < m; j++)
+            if (aux[j] != aux[i])
+                break;
+
+        if (sz == 0
+            || (j-i >= bca->min_support &&
+                // Note, doesn't handle bca->per_sample_flt yet
+                (bca->per_sample_flt
+                 || (double)(j-i) / n_tot >= bca->min_frac)))
+            types[t++] = sz;
+        i = j-1;
+    }
+    free(aux);
+
+    if (t <= 1) {
+        free(types);
+        return NULL;
+    }
+    n_types = t;
+
+    // Find reference type; types[?] == 0)
+    for (t = 0; t < n_types; ++t)
+        if (types[t] == 0) break;
+
+    *ref_type_r   = t;
+    *n_types_r    = n_types;
+    *max_rd_len_r = max_rd_len;
+    *N_r          = N;
+
+    return types;
+}
+
+// Increment ins["str"] and freq["str"]
+#define NI 100 // number of alternative insertion sequences
+// Could use a hash table too, but expectation is a tiny number of alternatives
+typedef struct {
+    char *str[NI];
+    int len[NI];
+    int freq[NI];
+} str_freq;
+
+static int bcf_cgp_append_cons(str_freq *sf, char *str, int len, int freq) {
+    int j;
+
+    for (j = 0; j < NI && sf->str[j]; j++) {
+        if (sf->len[j] == len && memcmp(sf->str[j], str, len) == 0)
+            break;
+    }
+    if (j >= NI)
+        return 0; // too many choices; discard
+
+    sf->freq[j]+=freq;
+    if (!sf->str[j]) {
+        // new insertion
+        if (!(sf->str[j] = malloc(len+1)))
+            return -1;
+        memcpy(sf->str[j], str, len);
+        sf->len[j] = len;
+    }
+
+    return 0;
+}
+
+/*
+ * Compute the consensus for a specific indel type at pos.
+ *
+ * left_shift is the number of inserted(+) or deleted(-) bases added to
+ * the consensus before we get to pos.  This is necessary so the alignment
+ * band is correct as it's expected to start at left/right edges in
+ * sync
+ *
+ * We accumulate into several buffers for counting base types:
+ * cons_base   - consensus of data with p->indel == type, bases or gap
+ * ref_base    - consensus of data with p->indel != type, bases or gap
+ * cons_ins    - consensus of data with p->indel == type, insertions
+ * ref_ins     - consensus of data with p->indel == type, bases or gap
+ *
+ * The purpose of cons_ins vs cons_base is if we have very low
+ * coverage due to nearly all reads being another type, then we can
+ * still get a robust consensus using the other data.  If we don't
+ * have shallow data, then we'll not use as much of ref_base as we may
+ * have correlated variants.
+ *
+ * Eg:
+ * REF: AGCTATGAGGCTGATA
+ * SEQ: AGGTAGGAGGGTGATA (x1)
+ * SEQ: AGCTACGAGG*TGATA (x24)
+ * SEQ: AGCTACTAGG*TGATA (x24)
+ *
+ * Cons for no-del is Cs not Gs.  Cannot trust it, so use N if shallow.
+ * CON: AGCTACNAGGGTGATA
+ *
+ * There are still some problems in cons_ins vs ref_ins assignment.
+ * We sometimes seem multiple similar-length insertions added at
+ * different locations.  Ideally we'd like to consider these as all
+ * the same insertion if the size is the same and it's comparable seq.
+ */
+#define MAX_INS 8192
+static char **bcf_cgp_consensus(int n, int *n_plp, bam_pileup1_t **plp,
+                                int pos, bcf_callaux_t *bca, const char *ref,
+                                int ref_len, int left, int right,
+                                int sample, int type, int biggest_del,
+                                int *left_shift, int *right_shift,
+                                int *band, int *tcon_len, int *cpos_pos,
+                                int pos_l, int pos_r) {
+    // Map ASCII ACGTN* to 012345
+    static uint8_t base6[256] = {
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        4,4,4,4,4,4,4,4,  4,4,5,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        //A   C       G       *^                     T
+        4,0,4,1,4,4,4,2,  4,4,4,4,4,4,4,4,  4,4,4,4,3,3,4,4,  4,4,4,4,4,4,4,4,
+        4,0,4,1,4,4,4,2,  4,4,4,4,4,4,4,4,  4,4,4,4,3,3,4,4,  4,4,4,4,4,4,4,4,
+
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+    };
+
+    // single base or del
+    int (*cons_base)[6] = calloc(right - left + 1, sizeof(*cons_base));
+    // multi-base insertions
+    str_freq *cons_ins  = calloc(right - left + 1, sizeof(*cons_ins));
+
+    // non-indel ref for all reads on this sample, rather than those just
+    // matching type.  We use this for handling the case where we have a
+    // homozygous deletion being studied, but with 1 or 2 reads misaligned
+    // and containing a base there.
+    //
+    // Eg if the type[]=0 consensus is made up of a very small sample size,
+    // which is also enriched for highly error prone data.  We can use
+    // the other reads from type[] != 0 to flesh out the consensus and
+    // improve accuracy.
+    int (*ref_base)[6]  = calloc(right - left + 1, sizeof(*ref_base));
+    str_freq *ref_ins   = calloc(right - left + 1, sizeof(*ref_ins));
+    int i, j, k, s = sample;
+    char **cons = NULL;
+
+    if (!cons_base || !cons_ins || !ref_base || !ref_ins)
+        goto err;
+
+    //--------------------------------------------------
+    // Accumulate sequences into cons_base and cons_ins arrays
+    int local_band_max = 0; // maximum absolute deviation from diagonal
+    int total_span_str = 0;
+    int type_depth = 0;
+    for (i = 0; i < n_plp[s]; i++) {
+        const bam_pileup1_t *p = plp[s] + i;
+        bam1_t *b = p->b;
+        int x = b->core.pos;  // ref coordinate
+        int y = 0;            // seq coordinate
+        uint32_t *cigar = bam_get_cigar(b);
+        uint8_t *seq = bam_get_seq(b);
+
+        int local_band = 0; // current deviation from diagonal
+        for (k = 0; k < b->core.n_cigar; ++k) {
+            int op  = cigar[k] &  BAM_CIGAR_MASK;
+            int len = cigar[k] >> BAM_CIGAR_SHIFT;
+            int base;
+            int skip_to = 0;
+
+            switch(op) {
+            case BAM_CSOFT_CLIP:
+                y += len;
+                break;
+
+            case BAM_CMATCH:
+            case BAM_CEQUAL:
+            case BAM_CDIFF: {
+                // Can short-cut this with j_start and j_end based on
+                // x+len and left,right
+                for (j = 0; j < len; j++, x++, y++) {
+                    if (x < left) continue;
+                    if (x >= right) break;
+
+                    base = bam_seqi(seq, y);
+                    if (p->indel == type)
+                        // Convert 4-bit base ambig code to 0,1,2,3,4 range
+                        cons_base[x-left][seq_nt16_int[base]]++;
+                    else if (x != pos+1) // indel being assessed question
+                        ref_base[x-left][seq_nt16_int[base]]++;
+                }
+                break;
+            }
+
+            case BAM_CINS: {
+                if (x >= left && x < right) {
+                    local_band += p->indel;
+                    if (local_band_max < local_band)
+                        local_band_max = local_band;
+                }
+
+                char ins[MAX_INS];
+                for (j = 0; j < len; j++, y++) {
+                    if (x < left) continue;
+                    if (x >= right)
+                        break;
+                    base = bam_seqi(seq, y);
+                    if (j < MAX_INS)
+                        ins[j] = seq_nt16_int[base];
+                }
+
+                // Insertions come before a ref match.
+                // 5I 5M is IIIIIM M M M M events, not
+                // {IIIII,M} M M M M choice.  So we need to include the
+                // next match in our sequence when choosing the consensus.
+                if (x >= left && x < right) {
+                    int ilen = j<MAX_INS?j:MAX_INS;
+                    if (p->indel == type /*&& x == pos+1*/) {
+                        // Assume any ins of the same size is the same ins.
+                        // (This rescues misaligned insertions.)
+                        if (bcf_cgp_append_cons(&cons_ins[x-left], ins,
+                                                ilen, 1) < 0)
+                            goto err;
+                        type_depth += (x == pos+1);
+                    } else  if (x != pos+1){
+                        if (bcf_cgp_append_cons(&ref_ins[x-left],  ins,
+                                                ilen, 1) < 0)
+                            goto err;
+                    }
+                }
+                break;
+            }
+
+            case BAM_CDEL:
+                if (x >= left && x < right) {
+                    local_band += p->indel;
+                    if (local_band_max < -local_band)
+                        local_band_max = -local_band;
+                }
+
+                // Maybe not perfect for I/D combos, but likely sufficient.
+                for (j = 0; j < len; j++, x++) {
+                    if (x < left) continue;
+                    if (x >= right) break;
+                    if ((p->indel == type && !p->is_del) ||  // starts here
+                        (p->indel == 0 && p->is_del && len == -type)) { // left
+                        cons_base[x-left][5]++;
+                        type_depth += (x == pos+1);
+                    } else if (x+len <= pos+1 || (skip_to && x > skip_to))
+                        ref_base[x-left][5]++;
+                    else if (x <= pos && x+len > pos+1) {
+                        // we have a deletion which overlaps pos, but
+                        // isn't the same "type".  We don't wish to
+                        // include these as they may bias the
+                        // evaluation by confirming against a
+                        // secondary consensus produced with the other
+                        // deletion.  We set a marker for how long to
+                        // skip adding to ref_base.
+                        if (x > skip_to)
+                            skip_to = x+len;
+                    }
+                }
+                break;
+            }
+        }
+
+        if (b->core.pos <= pos_l && x >= pos_r)
+            total_span_str++;
+
+        // Also track the biggest deviation +/- from diagonal.  We use
+        // this band observation in our BAQ alignment step.
+        if (*band < local_band_max)
+            *band = local_band_max;
+    }
+
+    //--------------------------------------------------
+    // Expand cons_base to include depth from ref_base/ref_ins
+    // Caveat: except at pos itself, where true ref is used if type != 0
+
+#if 1 // TEST 1
+    // We could retest this heuristic further maybe.
+    for (i = 0; i < right-left; i++) {
+        // Total observed depth
+        int t = cons_base[i][0] + cons_base[i][1] + cons_base[i][2] +
+            cons_base[i][3] + cons_base[i][4] + cons_base[i][5];
+        for (j = 0; j < NI; j++) {
+            if (!cons_ins[i].str[j])
+                break;
+            t += cons_ins[i].freq[j];
+        }
+
+        // Similarly for depth on the non-ALT calls (NB: not necessarily
+        // REF as maybe it's other ALTs).
+        int r = ref_base[i][0] + ref_base[i][1] + ref_base[i][2] +
+            ref_base[i][3] + ref_base[i][4] + ref_base[i][5];
+        for (j = 0; j < NI; j++) {
+            if (!ref_ins[i].str[j])
+                break;
+            r += ref_ins[i].freq[j];
+        }
+
+        // When evaluating this particular indel, we don't want to
+        // penalise alignments by SNP errors elsewhere.  This can
+        // happen when we have low depth for a particular 'type'.
+        //
+        // So add in a little data from ref_base/ref_ins.
+        double rfract = (r - t*2)*.75 / (r+1);
+
+        if (rfract < 1.01 / (r+1e-10))
+            rfract = 1.01 / (r+1e-10); // low depth compensation
+//        if (rfract > 0.2)
+//            rfract = 0.2;
+
+        // TODO: consider limiting rfract so we never drown out the
+        // signal.  We want to use the remaining data only to correct
+        // for sequencing errors in low depth alleles.  If we get
+        // conflicts, it's better to use N than to change a base
+        // incase that variant is genuine.
+        if (i+left >= pos+1 && i+left < pos+1-biggest_del) {
+            // We're overlapping the current indel region, so
+            // we don't wish to bring in evidence from the other
+            // "type" data as it'll harm calling.
+            continue;
+        } else {
+            // Otherwise add in a portion of other data to
+            // boost low population numbers.
+            cons_base[i][0] += rfract * ref_base[i][0];
+            cons_base[i][1] += rfract * ref_base[i][1];
+            cons_base[i][2] += rfract * ref_base[i][2];
+            cons_base[i][3] += rfract * ref_base[i][3];
+            cons_base[i][4] += rfract * ref_base[i][4];
+            cons_base[i][5] += rfract * ref_base[i][5];
+        }
+
+        // Similarly for insertions too; consider a different rfract here?
+        for (j = 0; j < NI; j++) {
+            if (!ref_ins[i].str[j])
+                break;
+            if (bcf_cgp_append_cons(&cons_ins[i],
+                                    ref_ins[i].str[j], ref_ins[i].len[j],
+                                    rfract * ref_ins[i].freq[j]) < 0)
+                goto err;
+        }
+    }
+#endif
+
+    //--------------------------------------------------
+    // Allocate consensus buffer, to worst case length
+    int max_len = right-left;
+    for (i = 0; i < right-left; i++) {
+        if (!cons_ins[i].str[0])
+            continue;
+
+        int ins = 0;
+        for (j = 0; j < NI; j++) {
+            if (!cons_ins[i].str[j])
+                break;
+            if (cons_ins[i].str[j] && ins < cons_ins[i].len[j])
+                ins = cons_ins[i].len[j];
+        }
+        max_len += ins;
+    }
+    max_len += MAX(0, type); // incase type inserted bases never occur
+    cons = malloc((max_len+1)*2 + sizeof(char *)*2);
+    if (!cons)
+        goto err;
+    cons[0] = (char *)&cons[2];
+    cons[1] = cons[0] + max_len+1;
+
+    //--------------------------------------------------
+    // Merge insertions where they are the same length but different
+    // sequences.
+    // NB: we could just index by length and have accumulators for each,
+    // instead of storing separately and merging later (here).
+    // Ie str_freq.str is [NI][5] instead.
+    for (i = 0; i < right-left; i++) {
+        int ins[MAX_INS][5];
+        for (j = 0; j < NI; j++) {
+            if (!cons_ins[i].str[j])
+                break;
+
+            if (cons_ins[i].freq[j] == 0)
+                continue; // already merged
+
+            int l;
+            for (l = 0; l < cons_ins[i].len[j]; l++) {
+                // Append to relevant frequency counter, zero all others
+                ins[l][0] = ins[l][1] = ins[l][2] = ins[l][3] = ins[l][4] = 0;
+                uint8_t b = cons_ins[i].str[j][l];
+                ins[l][b] = cons_ins[i].freq[j];
+            }
+
+            // Merge other insertions of the same length to ins[] counters
+            for (k = j+1; k < NI; k++) {
+                if (!cons_ins[i].str[k])
+                    break;
+                if (cons_ins[i].len[k] != cons_ins[i].len[j])
+                    continue;
+                if (cons_ins[i].freq[k] == 0)
+                    continue; // redundant?
+
+                // Merge str[j] and str[k]
+                for (l = 0; l < cons_ins[i].len[k]; l++) {
+                    uint8_t b = cons_ins[i].str[k][l];
+                    ins[l][b] += cons_ins[i].freq[k];
+                }
+                cons_ins[i].freq[j] += cons_ins[i].freq[k];
+                cons_ins[i].freq[k] = 0;
+            }
+
+            // Now replace ins[j] with the consensus insertion of this len.
+            for (l = 0; l < cons_ins[i].len[j]; l++) {
+                int max_v = 0, base = 0;
+                int tot = ins[l][0] + ins[l][1] + ins[l][2]
+                        + ins[l][3] + ins[l][4];
+                if (max_v < ins[l][0]) max_v = ins[l][0], base = 0;
+                if (max_v < ins[l][1]) max_v = ins[l][1], base = 1;
+                if (max_v < ins[l][2]) max_v = ins[l][2], base = 2;
+                if (max_v < ins[l][3]) max_v = ins[l][3], base = 3;
+                if (max_v < ins[l][4]) max_v = ins[l][4], base = 4;
+
+                cons_ins[i].str[j][l] = (max_v > 0.6*tot) ? base : 4;
+            }
+        }
+    }
+
+#define CONS_CUTOFF      .40 // % needed for base vs N
+#define CONS_CUTOFF_DEL  .35 // % to include any het del
+#define CONS_CUTOFF2     .80 // % needed for gap in cons[1]
+#define CONS_CUTOFF_INC  .35 // % to include any insertion cons[0]
+#define CONS_CUTOFF_INC2 .80 // % to include any insertion cons[1] HOM
+#define CONS_CUTOFF_INS  .60 // and then 60% needed for it to be bases vs N
+
+    //--------------------------------------------------
+    // Walk through the frequency arrays to call the consensus.
+    // We produce cons[0] and cons[1].  Both include strongly
+    // homozygous indels.  Both also include the indel at 'pos'.
+    // However for heterozygous indels we call the most likely event
+    // for cons[0] and the less-likely alternative in cons[1].
+    // TODO: a proper phase analysis so multiple events end up
+    // combining together into the correct consensus.
+    *left_shift = 0;
+    *right_shift = 0;
+    int cnum;
+
+    // Het call filled out in cnum==0 (+ve or -ve).
+    // Used in cnum==1 to do the opposite of whichever way we did before.
+    int heti[MAX_INS] = {0}, hetd[MAX_INS] = {0};
+
+    *cpos_pos = -1;
+    for (cnum = 0; cnum < 2; cnum++) {
+        for (i = k = 0; i < right-left; i++) {
+            // Location in consensus matching the indel itself
+            if (i >= pos-left+1 && *cpos_pos == -1)
+                *cpos_pos = k;
+
+            int max_v = 0, max_v2 = 0, max_j = 4, max_j2 = 4, tot = 0;
+            for (j = 0; j < 6; j++) {
+                // Top 2 consensus calls
+                if (max_v < cons_base[i][j]) {
+                    max_v2 = max_v, max_j2 = max_j;
+                    max_v = cons_base[i][j], max_j = j;
+                } else if (max_v2 < cons_base[i][j]) {
+                    max_v2 = cons_base[i][j], max_j2 = j;
+                }
+                tot += cons_base[i][j];
+            }
+
+            // +INS
+            int max_v_ins = 0, max_j_ins = 0;
+            int tot_ins = 0;
+            for (j = 0; j < NI; j++) {
+                if (i+left==pos+1)
+                if (type > 0 && i+left == pos+1
+                    && cons_ins[i].len[j] < type && j == 0) {
+                    cons_ins[i].str[j] = realloc(cons_ins[i].str[j], type);
+                    if (!cons_ins[i].str[j])
+                        goto err;
+                    memset(cons_ins[i].str[j] + cons_ins[i].len[j],
+                           4, type - cons_ins[i].len[j]);
+                    cons_ins[i].len[j] = type;
+                }
+                if (!cons_ins[i].str[j])
+                    break;
+                if (cons_ins[i].freq[j] == 0)
+                    continue; // previously merged
+
+                if (max_v_ins < cons_ins[i].freq[j])
+                    //if (i != pos-left+1 || cons_ins[i].len[j] == type)
+                    max_v_ins = cons_ins[i].freq[j], max_j_ins = j;
+                tot_ins += cons_ins[i].freq[j];
+            }
+
+            // NB: tot is based on next matching base, so it includes
+            // everything with or without the insertion.
+            int tot_sum = tot;
+            int always_ins =
+                (i == pos-left+1 && type>0) ||       // current eval
+                max_v_ins > CONS_CUTOFF_INC2*tot_sum;// HOM
+            int het_ins = 0;
+            if (!always_ins && max_v_ins >= bca->min_support) {
+                // Candidate HET ins.
+                if (cnum == 0) {
+                    het_ins = max_v_ins > CONS_CUTOFF_INC * tot_sum;
+                    if (i < MAX_INS) heti[i] = het_ins
+                                      ? 1
+                                      : (max_v_ins > .3*tot_sum ? -1:0);
+                } else {
+                    // HET but uncalled before
+                    het_ins = i < MAX_INS ? (heti[i] == -1) : 0;
+                }
+            }
+
+            if (always_ins || het_ins) {
+                if (max_v_ins > CONS_CUTOFF_INS*tot_ins) {
+                    // Insert bases
+                    for (j = 0; j < cons_ins[i].len[max_j_ins]; j++) {
+                        if (cnum == 0) {
+                            if (k < pos-left+*left_shift)
+                                (*left_shift)++;
+                            else
+                                (*right_shift)++;
+                        }
+                        cons[cnum][k++] = cons_ins[i].str[max_j_ins][j];
+                    }
+                } else {
+                    for (j = 0; j < cons_ins[i].len[max_j_ins]; j++)
+                        cons[cnum][k++] = 4; // 'N';
+                }
+            }
+
+            // Call deletions & bases
+            int always_del = (type < 0 && i > pos-left && i <= pos-left-type)
+                || cons_base[i][5] > CONS_CUTOFF2 * tot; // HOM del
+            int het_del = 0;
+            if (!always_del && cons_base[i][5] >= bca->min_support) {
+                // Candidate HET del.
+                if (cnum == 0) {
+                    int tot2 = tot;
+                    if (i > pos-left && i <= pos-left-biggest_del)
+                        tot2 = total_span_str - type_depth;
+                    het_del = cons_base[i][5] >= CONS_CUTOFF_DEL * tot2;
+
+                    if (i < MAX_INS) {
+                        if (i > pos-left && i <= pos-left-biggest_del)
+                            hetd[i] = 0;
+                        else
+                            hetd[i] = het_del
+                                ? 1
+                                : (cons_base[i][5] >= .3 * tot2 ? -1 : 0);
+                    }
+                } else {
+                    // HET del uncalled on cnum 0
+                    het_del = i < MAX_INS ? (hetd[i] == -1) : 0;
+                    if (max_j == 5 && het_del == 0) {
+                        max_v = max_v2;
+                        max_j = max_j2;
+                    }
+                }
+            }
+            if (always_del || het_del) {
+                // Deletion
+                if (k < pos-left+*left_shift)
+                    (*left_shift)--;
+                else
+                    (*right_shift)++;
+            } else {
+                // Finally the easy case - a non-indel base or an N
+                if (max_v > CONS_CUTOFF*tot)
+                    cons[cnum][k++] = max_j; // "ACGTN*"
+                else if (max_v > 0)
+                    cons[cnum][k++] = 4;     // 'N';
+                else {
+                    cons[cnum][k] = left+k < ref_len
+                        ? base6[(uint8_t)ref[left+k]]
+                        : 4;
+                    k++;
+                }
+            }
+        }
+
+        tcon_len[cnum] = k;
+    }
+
+    // TODO: replace by io_lib's string pool for rapid tidying.
+    // For now this isn't the bottleneck though.
+    for (i = 0; i < right-left; i++) {
+        for (j = 0; j < NI; j++) {
+            if (cons_ins[i].str[j])
+                free(cons_ins[i].str[j]);
+            if (ref_ins[i].str[j])
+                free(ref_ins[i].str[j]);
+        }
+    }
+
+ err:
+    free(cons_base);
+    free(ref_base);
+    free(cons_ins);
+    free(ref_ins);
+
+    return cons;
+}
+
+// A rename of bcf_cgp_calc_cons from bam2bcf_indel.c
+//
+// Compute the insertion consensus for this sample 's' via a basic
+// majority rule.
+//
+// TODO: merge this into bcf_cgp_consensus as another return value?
+static char *bcf_cgp_calc_ins_cons(int n, int *n_plp, bam_pileup1_t **plp,
+                                   int pos, int *types, int n_types,
+                                   int max_ins, int s) {
+    return bcf_cgp_calc_cons(n, n_plp, plp, pos, types, n_types, max_ins, s);
+}
+
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#define MIN(a,b) ((a)<(b)?(a):(b))
+
+// Compile with LIBS="-L. -ldl -ledlib" CLD=g++
+
+// This is faster than ksw and BAQ, meaning we can use larger --indel-size and
+// get a more accurate context, improving alignments further.  This *may*
+// compensate for reduced sensitivity.
+#include "edlib.h"
+int edlib_glocal(uint8_t *ref, int l_ref, uint8_t *query, int l_query,
+                 double m, double del_bias)
+{
+    EdlibAlignConfig cfg = 
+        edlibNewAlignConfig(
+                            //ABS(type)+ABS(l_ref-l_query)+10,
+                            -1, // k; use small positive for faster alignment
+                            EDLIB_MODE_HW, // mode
+#ifdef ALIGN_DEBUG
+                            EDLIB_TASK_PATH,
+#else
+                            EDLIB_TASK_LOC,
+#endif
+                            NULL, // additionalEqualities
+                            0); // additionalEqualitiesLength
+    EdlibAlignResult r = 
+        edlibAlign((char *)query, l_query, (char *)ref, l_ref, cfg);
+
+    if (r.status != EDLIB_STATUS_OK || r.numLocations < 1 ||
+        !r.endLocations || !r.startLocations) {
+        edlibFreeAlignResult(r);
+        return INT_MAX;
+    }
+
+#ifdef ALIGN_DEBUG
+    // NB: Needs linking against the C++ libedlib.a as our cut-down C
+    // implementation misses the alignment generation code.
+    {
+        int i, j = 0, pt = r.startLocations[0], pq = 0;
+        char line1[80];
+        char line2[80];
+        char line3[80];
+        for (i = 0; i < r.alignmentLength && pt < r.endLocations[0]; i++) {
+            int n;
+            switch (n = r.alignment[i]) {
+            case 0: // match
+            case 3: // mismatch
+                line1[j] = "ACGTN"[ref[pt++]];
+                line2[j] = "ACGTN"[query[pq++]];
+                line3[j] = " x"[n==3];
+                break;
+            case 2: // insertion to ref
+                line1[j] = "ACGTN"[ref[pt++]];
+                line2[j] = '-';
+                line3[j] = '-';
+                break;
+            case 1: // insertion to query
+                line1[j] = '-';
+                line2[j] = "ACGTN"[query[pq++]];
+                line3[j] = '+';
+                break;
+            }
+
+            if (++j == sizeof(line1)) {
+                fprintf(stderr, "%.*s\n", j, line1);
+                fprintf(stderr, "%.*s\n", j, line2);
+                fprintf(stderr, "%.*s\n", j, line3);
+                j = 0;
+            }
+        }
+        if (j) {
+            fprintf(stderr, "%.*s\n", j, line1);
+            fprintf(stderr, "%.*s\n", j, line2);
+            fprintf(stderr, "%.*s\n", j, line3);
+        }
+    }
+#endif
+
+    // Aligned target length minus query length is an indication of the number
+    // of insertions and/or deletions.
+    // 
+    // For CIGAR 10M1I10M t_len > l_query ("AC"  / "ATC")
+    // For CIGAR 10M1D10M t_len < l_query ("ATC" / "AC")
+    // Hence t_len-l_query is -ve for net insertions and +ve for net deletions.
+    // If we compute nins and ndel directly via walking though EDLIB_TASK_PATH
+    // we'll see t_len-l_query == ndel-nins.
+    // 
+    // If a technology has a significantly higher chance of making deletion
+    // errors than insertion errors, then we would view deletions as less
+    // indicative of this sequence not coming from this candidate allele than
+    // if it had insertion (as the deletions are more likely to be errors
+    // rather than real, relative to the insertions).  Hence we can skew the
+    // score by the net delta of num_del - num_ins.
+    //
+    // Note this is an approximation that doesn't account for multiple
+    // insertions and deletions within the same sequence, but it is much faster
+    // as it doesn't require EDLIB_TASK_PATH to be computed.
+    //
+    // Given editDistance is +1 for every mismatch, insertion and deletion,
+    // provided the t_len-l_query multiplier < 1 then this is always +ve.
+
+    int t_len = *r.endLocations - *r.startLocations + 1;
+    int score = m*(r.editDistance - del_bias*(t_len - l_query));
+
+    edlibFreeAlignResult(r);
+    return score;
+}
+
+// Part of bcf_call_gap_prep.
+//
+// Realign using BAQ to get an alignment score of a single read vs
+// a haplotype consensus.  TODO: replace BAQ with something more robust.
+//
+// There are many coordinates, so let's explain them.
+// - left, right, tbeg, tend, r_start and r_end are in aligned reference
+//   coordinates.
+//   left/right start from pos +/- indel_win_size.
+//   r_start/r_end are the BAM first and last mapped coord on the reference.
+//   tbeg and tend are the intersection of the two.
+// - qbeg and qend are in BAM sequence coordinates
+// - qpos is in sequence coordinates, relative to qbeg.
+//
+// To see what this means, we have illustrations with coordinates
+// above the seqs in reference space and below the seqs in BAM seq space.
+//
+// Overlap left:
+//                     tbeg                        tend
+//      r_start        left                 pos    r_end          right
+// REF  :..............|--------------------#------:--------------|...
+// SEQ  :..............|--------------------#------|
+//      0              qbeg                 qpos   qend
+//
+// Overlap right:
+//                        r_start                     tend
+//         left           tbeg  pos                   right       r_end
+// REF  ...|--------------:-----#---------------------|...........:
+// SEQ                    |-----#---------------------|...........:
+//                        qbeg  qpos                  qend
+//                        0
+//
+// The "-" sequence is the bit passed in.
+// Ie ref2 spans left..right and query spans qbeg..qend.
+// We need to adjust ref2 therefore to tbeg..tend.
+//
+// Fills out score
+// Returns 0 on success,
+//        <0 on error
+static int bcf_cgp_align_score(bam_pileup1_t *p, bcf_callaux_t *bca,
+                               int type, int band,
+                               uint8_t *ref1, uint8_t *ref2, uint8_t *query,
+                               int r_start, int r_end,
+                               int tbeg, int tend1, int tend2,
+                               int left, int right,
+                               int qbeg, int qend,
+                               int pos, int qpos, int max_deletion,
+                               double qavg, double del_bias, int *score,
+                               int *str_len1_p, int *str_len2_p) {
+    int atype = abs(type);
+    int l, sc1, sc2;
+
+    // Trim poly_Ns at ends of ref.
+    // This helps to keep len(ref) and len(query) similar, to reduce
+    // band size and reduce the chance of -ve BAQ scores.
+    for (l = 0; l < tend1-tbeg && l < tend2-tbeg; l++)
+        if (ref1[l + tbeg-left] != 4 || ref2[l + tbeg-left] != 4)
+            break;
+    if (l > atype)
+        tbeg += l-atype;
+
+    for (l = tend1-tbeg-1; l >= 0; l--)
+        if (ref1[l + tbeg-left] != 4)
+            break;
+    l = tend1-tbeg-1 - l;
+    if (l > atype)
+        tend1 -= l-atype;
+
+    for (l = tend2-tbeg-1; l >= 0; l--)
+        if (ref2[l + tbeg-left] != 4)
+            break;
+    l = tend2-tbeg-1 - l;
+    if (l > atype) {
+        tend2 -= l-atype;
+    }
+
+    // The bottom 8 bits are length-normalised score while
+    // the top bits are unnormalised.
+    //
+    // Try original cons and new cons and pick best.
+    // This doesn't reduce FN much (infact maybe adds very slightly),
+    // but it does reduce GT errors and is a slight reduction to FP.
+
+    double mm = 30; // a const average qual for now. Could tune
+    sc2 = edlib_glocal(ref2 + tbeg - left, tend2 - tbeg,
+                       query, qend - qbeg, mm, del_bias);
+
+    if (tend1 != tend2 ||
+        memcmp((char *)ref1 + tbeg - left, (char *)ref2 + tbeg - left,
+               tend1 - tbeg) != 0)
+        sc1 = edlib_glocal(ref1 + tbeg - left, tend1 - tbeg,
+                           query, qend - qbeg, mm, del_bias);
+    else
+        sc1 = INT_MAX; // skip
+
+    // Find the best of the two alignments
+    if (sc1 < 0 && sc2 < 0) {
+        *score = 0xffffff;
+        return 0;
+    }
+    if (sc1 < 0) {
+        // sc2 is already correct
+    } else if (sc2 < 0) {
+        sc2 = sc1;
+    } else {
+        // sc1 and sc2 both pass, so use best
+        if (sc2 > sc1)
+            sc2 = sc1;
+    }
+
+    // Sc is overall alignment score, in top 24 bits (SeqQ). It's based
+    // purely on the scores for the whole alignment.
+    // We also have a separate indel score in bottom 8 bits (IndelQ).
+    // This is a function of all sorts of attributes of the candidate indel
+    // itself, such as STR length and the presence of poor quality bases.
+
+    // Used for adjusting indelQ below.  Lower l is more likely to call
+    // (--FN, ++FP).  (NB CLI --indel_bias is 1/indel_bias var).
+    // Starts as average score per base, and then adjusted based on seq
+    // complexity / quality.
+
+    l = .5*(100. * sc2 / (qend - qbeg) + .499);
+
+    *score = (sc2<<8) | (int)MIN(255, l * bca->indel_bias * .5);
+
+    return 0;
+}
+
+// Part of bcf_call_gap_prep.
+//
+// Returns n_alt on success
+//         -1 on failure
+
+// TODO: almost identical to bam2bcf_indel.c's copy, so we could share
+// the code and add a check on bca->edlib.
+static int bcf_cgp_compute_indelQ(int n, int *n_plp, bam_pileup1_t **plp,
+                                  bcf_callaux_t *bca, char *inscns,
+                                  int l_run, int max_ins,
+                                  int ref_type, int *types, int n_types,
+                                  double qavg, int *score,
+                                  int str_len1, int str_len2) {
+    // FIXME: n_types has a maximum; no need to alloc - use a #define?
+    int sc[MAX_TYPES], sumq[MAX_TYPES], s, i, j, t, K, n_alt, tmp;
+    memset(sumq, 0, n_types * sizeof(int));
+    int sum_indelQ1[100] = {0}; // n
+    int sum_indelQ2[100] = {0}; // n
+
+    // Confusing variable naming and bit usage.
+    //
+    // score[] is low 8  bits normalised (by len) alignment score
+    //            top 24 bits full alignment score
+    // This gets cast into "sct"; mnemonic score-per-indel-type.
+    //
+    // sc = (score<<6) | type  (index to types[] array for indel size)
+    // So sc>>14 = score>>(14-6) = score>>8.  Ie full alignment score
+    for (s = K = 0; s < n; ++s) {
+        for (i = 0; i < n_plp[s]; ++i, ++K) {
+            bam_pileup1_t *p = plp[s] + i;
+            // Labelling is confusing here.
+            //    sct is short for score.
+            //    sc is score + t(type)
+            // Why aren't these variable names reversed?
+            int *sct = &score[K*n_types], seqQ, indelQ1=0, indelQ2=0, indelQ=0;
+            for (t = 0; t < n_types; ++t) sc[t] = sct[t]<<6 | t;
+            for (t = 1; t < n_types; ++t) // insertion sort
+                for (j = t; j > 0 && sc[j] < sc[j-1]; --j)
+                    tmp = sc[j], sc[j] = sc[j-1], sc[j-1] = tmp;
+
+#ifdef ALIGN_DEBUG
+            fprintf(stderr, "READ %s\tscores ", bam_get_qname(p->b));
+            for (t = 0; t < n_types; ++t) {
+                fprintf(stderr, "%+2d/%-3d ", types[sc[t]&0x3f], sc[t]>>14);
+            }
+#endif
+
+            /* errmod_cal() assumes that if the call is wrong, the
+             * likelihoods of other events are equal. This is about
+             * right for substitutions, but is not desired for
+             * indels. To reuse errmod_cal(), I have to make
+             * compromise for multi-allelic indels.
+             */
+            if ((sc[0]&0x3f) == ref_type) {
+                // sc >> 14 is the total score.  It's been shifted by 8
+                // from normalised score and 6 from type.
+                // &0x3f is type number
+
+                // Best call is REF.  Compare vs best indel
+                indelQ = (sc[1]>>14) - (sc[0]>>14);
+                seqQ = est_seqQ(bca, types[sc[1]&0x3f], l_run, str_len1);
+            } else {
+                // look for the reference type
+                for (t = 0; t < n_types; ++t) {
+                    if ((sc[t]&0x3f) == ref_type)
+                        break;
+                }
+                indelQ = indelQ1 = (sc[t]>>14) - (sc[0]>>14);
+//                fprintf(stderr, "IndelQ = %d: %d-%d",
+//                        indelQ, (sc[t]>>14), (sc[0]>>14));
+
+                // Best call is non-ref, compare vs next best non-ref,
+                // or ref if it's just 2 choices (most common case).
+                for (t = 1; t < n_types; t++)
+                    if ((sc[t]&0x3f) == ref_type)
+                        continue;
+                    else break;
+                if (t == n_types)
+                    t--; // it's ref, but it'll do as next best.
+                indelQ2 = (sc[t]>>14) - (sc[0]>>14);
+                seqQ = est_seqQ(bca, types[sc[0]&0x3f], l_run, str_len1);
+
+#if 1 // TEST 3
+                indelQ = bca->vs_ref*indelQ1 + (1-bca->vs_ref)*indelQ2;
+#endif
+            }
+
+            // So we lower qual in some, but raise the average to keep FN/FP
+            // ratios up.
+            // Is this key diff for PacBio old vs new HiFi?
+            indelQ  /= bca->indel_bias*0.5;
+            indelQ1 /= bca->indel_bias*0.5;
+
+            // Or maybe just *2 if bca->poly_mqual and be done with it?
+            // Or perhaps adjust the MIN(qavg/20, ...) to MIN(qavg/10) ?
+
+            // Skew SeqQ and IndelQ based on a portion of the minimum quality
+            // found within a homopolymer.  This is useful where the quality
+            // values are a bit mutable and move around in such data, but less
+            // so on clocked sequencing technologies.
+            //
+            // Enabling this causes lots of GT errors on Illumina.
+            // However on PacBio it's key to removal of false positives.
+            // ONT and UG seem somewhere inbetween.
+            if (bca->poly_mqual) { // TEST 4
+                int qpos = p->qpos, l;
+                uint8_t *seq = bam_get_seq(p->b);
+                uint8_t *qual = bam_get_qual(p->b);
+                int min_q = qual[qpos];
+
+                // scan homopolymer left
+                char baseL = bam_seqi(seq, qpos+1 < p->b->core.l_qseq
+                                      ? qpos+1 : qpos);
+                for (l = qpos; l >= 0; l--) {
+                    if (bam_seqi(seq, l) != baseL)
+                        break;
+                    if (min_q > qual[l])
+                        min_q = qual[l];
+                }
+
+                // scan homo-polymer right (including site of indel)
+                char base = bam_seqi(seq, qpos+1);
+                for (l = qpos+1; l < p->b->core.l_qseq; l++) {
+                    if (min_q > qual[l])
+                        min_q = qual[l];
+                    if (bam_seqi(seq, l) != base)
+                        break;
+                }
+
+                // We reduce -h so homopolymers get reduced likelihood of being
+                // called, but then optionally increase or decrease from there
+                // based on base quality.  Hence lack of low quality bases in
+                // homopolymer will rescue the score back again, reducing FNs.
+
+                // The score factors here may also be machine specific, but for
+                // now these work well (tuned on PB HiFi).
+                seqQ   += MIN(qavg/20,  min_q - qavg/10);
+                indelQ += MIN(qavg/20,  min_q - qavg/5);
+                indelQ1+= MIN(qavg/20,  min_q - qavg/5);
+
+                if (seqQ   < 0) seqQ   = 0;
+                if (indelQ < 0) indelQ = 0;
+                if (indelQ1< 0) indelQ1= 0;
+            }
+
+            // This is the length-normalised score from bcf_cgp_align_score
+            tmp = sc[0]>>6 & 0xff;
+
+            // reduce indelQ
+            // high score = bad, low score = good; flip for indelQ
+            // low normalised scores leave indelQ unmodified
+            // high normalised scores set indelQ to 0
+            // inbetween scores have a linear scale from indelQ to 0
+// Altering the MAGIC value below (originally 111, but chosen for unknown
+// reasons) is comparable to altering --indel-bias.
+#define TMP_MAGIC 255.0
+
+            indelQ = tmp > TMP_MAGIC? 0 : (int)((1. - tmp/TMP_MAGIC) * indelQ + .499);
+            indelQ1= tmp > TMP_MAGIC? 0 : (int)((1. - tmp/TMP_MAGIC) * indelQ1+ .499);
+
+            indelQ  = MIN(indelQ,  255);
+            indelQ1 = MIN(indelQ1, 255);
+
+            // Doesn't really help accuracy, but permits -h to take
+            // affect still.
+            if (indelQ > seqQ) indelQ = seqQ;
+            if (indelQ > 255) indelQ = 255;
+            if (indelQ1> 255) indelQ1= 255;
+            if (seqQ > 255) seqQ = 255;
+
+            // Use 22 bits in total.
+            // 0-7   IndelQ
+            // 8-15  SeqQ
+            // 16-22 Score-per-base
+            p->aux = (sc[0]&0x3f)<<16 | seqQ<<8 | indelQ;
+            sumq[sc[0]&0x3f] += indelQ;
+
+#ifdef ALIGN_DEBUG
+            fprintf(stderr, "\t%d\t%d\n", indelQ, seqQ);
+#endif
+
+            // Experiment in p->aux vs sumq.
+            // One gives likelihood of an indel being here, while the other
+            // is likelihood of a specific genotype?  But which is which?
+
+            sum_indelQ1[s] += indelQ1;
+            sum_indelQ2[s] += indelQ;
+        }
+    }
+
+    // Determine bca->indel_types[] and bca->inscns.
+    // Sumq[0] is always reference.
+    // Sumq[1] is best non-ref (and maybe better than ref)
+    bca->maxins = max_ins;
+    bca->inscns = (char*) realloc(bca->inscns, bca->maxins * 4);
+    if (bca->maxins && !bca->inscns)
+        return -1;
+    for (t = 0; t < n_types; ++t)
+        sumq[t] = sumq[t]<<6 | t;
+    for (t = 1; t < n_types; ++t) // insertion sort
+        for (j = t; j > 0 && sumq[j] > sumq[j-1]; --j)
+            tmp = sumq[j], sumq[j] = sumq[j-1], sumq[j-1] = tmp;
+    for (t = 0; t < n_types; ++t) // look for the reference type
+        if ((sumq[t]&0x3f) == ref_type) break;
+
+    if (t) { // then move the reference type to the first
+        tmp = sumq[t];
+        for (; t > 0; --t) sumq[t] = sumq[t-1];
+        sumq[0] = tmp;
+    }
+
+    for (t = 0; t < 4; ++t) bca->indel_types[t] = B2B_INDEL_NULL;
+    for (t = 0; t < 4 && t < n_types; ++t) {
+        bca->indel_types[t] = types[sumq[t]&0x3f];
+#ifdef ALIGN_DEBUG
+        fprintf(stderr, "TYPE %+2d %d\n", types[t], sumq[t]>>6);
+#endif
+        if (bca->maxins) // potentially an insertion
+            memcpy(&bca->inscns[t * bca->maxins],
+                   &inscns[(sumq[t]&0x3f) * max_ins], bca->maxins);
+    }
+
+    // Update p->aux.
+    // If per-alignment type isn't found, then indelQ/seqQ is 0,
+    // otherwise unchanged.
+    for (s = n_alt = 0; s < n; ++s) {
+        for (i = 0; i < n_plp[s]; ++i) {
+            bam_pileup1_t *p = plp[s] + i;
+            int x = types[p->aux>>16&0x3f];
+            for (j = 0; j < 4; ++j)
+                if (x == bca->indel_types[j]) break;
+            p->aux = j<<16 | (j == 4? 0 : (p->aux&0xffff));
+            if ((p->aux>>16&0x3f) > 0) ++n_alt;
+#ifdef ALIGN_DEBUG
+            fprintf(stderr, "FIN %s\t%d\t%d\t%d\n",
+                    bam_get_qname(p->b), (p->aux>>16)&0x3f,
+                    bca->indel_types[(p->aux>>16)&0x3f], p->aux&0xff);
+#endif
+        }
+    }
+
+    return n_alt;
+}
+
+/*
+FIXME: with high number of samples, do we handle IMF correctly?  Is it
+fraction of indels across entire data set, or just fraction for this
+specific sample? Needs to check bca->per_sample_flt (--per-sample-mF) opt.
+ */
+
+/*
+    notes:
+    - n .. number of samples
+    - the routine sets bam_pileup1_t.aux of each read as follows:
+        - 6: unused
+        - 6: the call; index to bcf_callaux_t.indel_types   .. (aux>>16)&0x3f
+        - 8: estimated sequence quality                     .. (aux>>8)&0xff
+        - 8: indel quality                                  .. aux&0xff
+ */
+int bcf_edlib_gap_prep(int n, int *n_plp, bam_pileup1_t **plp, int pos,
+                      bcf_callaux_t *bca, const char *ref, int ref_len)
+{
+    if (ref == 0 || bca == 0) return -1;
+
+    int i, s, t, n_types, *types = NULL, max_rd_len, left, right, max_ins;
+    int *score = NULL;
+    int N, K, l_run, ref_type, n_alt = -1;
+    char *inscns = NULL, *query = NULL;
+
+    // determine if there is a gap
+    for (s = N = 0; s < n; ++s) {
+        for (i = 0; i < n_plp[s]; ++i)
+            if (plp[s][i].indel != 0) break;
+        if (i < n_plp[s]) break;
+    }
+    if (s == n)
+        // there is no indel at this position.
+        return -1;
+
+    // Find average base quality over this region
+    double qavg = 30, qsum = 0, qcount = 0;
+    int qmax = 0;
+    for (s = 0; s < n; s++) {
+        for (i = 0; i < n_plp[s]; i++) {
+#define QWIN 50
+            bam_pileup1_t *p = plp[s] + i;
+            int kstart = p->qpos - QWIN > 0 ? p->qpos - QWIN : 0;
+            int kend = p->qpos + QWIN < p->b->core.l_qseq
+                ? p->qpos + QWIN : p->b->core.l_qseq;
+            uint8_t *qual = bam_get_qual(p->b);
+            int k;
+            for (k = kstart; k < kend; k++) {
+                qsum += qual[k];
+                qcount++;
+                if (qmax < qual[k])
+                    qmax = qual[k];
+            }
+        }
+    }
+    qavg = (qsum+1) / (qcount+1);
+
+    // find out how many types of indels are present
+    types = bcf_cgp_find_types(n, n_plp, plp, pos, bca, ref,
+                               &max_rd_len, &n_types, &ref_type, &N);
+    if (!types)
+        goto err;
+
+
+    // calculate left and right boundary, based on type size for a bit more
+    // speed.
+    int max_indel = 20*MAX(ABS(types[0]), ABS(types[n_types-1]))
+                  + bca->indel_win_size/4;
+    if (max_indel > bca->indel_win_size)
+        max_indel = bca->indel_win_size;
+    left = pos > max_indel ? pos - max_indel : 0;
+    right = pos + max_indel;
+
+    int del_size = types[0]<0 ? -types[0] : 0;
+    right += del_size;
+
+    // in case the alignments stand out the reference
+    for (i = pos; i < right; ++i)
+        if (ref[i] == 0) break;
+    right = i;
+
+    // compute the likelihood given each type of indel for each read
+    max_ins = types[n_types - 1];   // max_ins is at least 0
+
+    // The length of the homopolymer run around the current position
+    l_run = bcf_cgp_l_run(ref, pos);
+    int l_run_base = seq_nt16_table[(uint8_t)ref[pos+1]];
+    int l_run_ins = 0;
+
+    // construct the consensus sequence (minus indels, which are added later)
+    if (max_ins > 0) {
+        // TODO: replace filling inscns[] with calc_consensus return
+        // so the merges of the insertion consensus for type[t] is
+        // reported directly.  (It may need adjustment to avoid N)
+        inscns = bcf_cgp_calc_ins_cons(n, n_plp, plp, pos,
+                                       types, n_types, max_ins, s);
+        if (!inscns)
+            return -1;
+    }
+
+    query = (char*) calloc(right - left + max_rd_len + max_ins + 2, 1);
+    score = (int*) calloc(N * n_types, sizeof(int));
+    bca->indelreg = 0;
+    double nqual_over_60 = bca->nqual / 60.0;
+
+    int biggest_del = 0;
+    int biggest_ins = 0;
+    for (t = 0; t < n_types; t++) {
+        if (biggest_del > types[t])
+            biggest_del = types[t];
+        if (biggest_ins < types[t])
+            biggest_ins = types[t];
+    }
+    int band = biggest_ins - biggest_del; // NB del is -ve
+
+    // Find left & right extents of STR covering pos, from ref
+    int pos_l = pos, pos_r = pos;
+    {
+        rep_ele *reps, *elt, *tmp;
+        int pstart = MAX(0, pos-30);
+        int pmid = pos-pstart;
+        int pend = MIN(ref_len, pos+30);
+        reps = find_STR((char *)&ref[pstart], pend-pstart, 0);
+        DL_FOREACH_SAFE(reps, elt, tmp) {
+            if (elt->end >= pmid && elt->start <= pmid) {
+                if (pos_l > pstart + elt->start)
+                    pos_l = pstart + elt->start;
+                if (pos_r < pstart + elt->end)
+                    pos_r = pstart + elt->end;
+            }
+            DL_DELETE(reps, elt);
+            free(elt);
+        }
+    }
+
+    int str_len1 = l_run, str_len2 = l_run/4;
+    for (t = 0; t < n_types; ++t) {
+        int l, ir;
+
+        // Compute indelreg.  This is the context in the reference.  Eg:
+        //
+        // REF:  AG--TTTC  Inscns   is "TT".
+        // SEQ:  AGTTTTTC  Indelreg is 3; next 3 "TTT" bases
+        //
+        // => GTTT GTTTTT is call.
+        if (types[t] == 0)
+            ir = 0;
+        else if (types[t] > 0)
+            ir = est_indelreg(pos, ref, types[t], &inscns[t*max_ins]);
+        else
+            ir = est_indelreg(pos, ref, -types[t], 0);
+
+        if (ir > bca->indelreg)
+            bca->indelreg = ir;
+
+        // Realignment score, computed via BAQ
+        for (s = K = 0; s < n; ++s) {
+            char **tcons;
+            int left_shift, right_shift;
+            int tcon_len[2];
+            int cpos_pos;
+            tcons = bcf_cgp_consensus(n, n_plp, plp, pos, bca, ref, ref_len,
+                                      left, right, s, types[t], biggest_del,
+                                      &left_shift, &right_shift, &band,
+                                      tcon_len, &cpos_pos, pos_l, pos_r);
+            // TODO: Consensus for a deletion shouldn't match the
+            // consensus for type 0.  Eg consider
+            //         vv                          vv
+            // REF:  AATGTGTGAACAA        REF:   AATGTG--AACAA
+            // T0:   AATGTG--AACAA        T0:    AATGTG--AACAA
+            // T-2:  AA--TGTGAATAA        T-2:   AA--TGTGAATAA:
+            //
+            // On left: both T0 and T-2 are the same length, as it's
+            // just a deletion that moved.  We may end up assigning
+            // reads to an indel allele based on the SNP they have and
+            // not the actual indel.
+            // There *is* a deletion here though, but only 1.  How do
+            // we call it once only?  Need to replace entire region
+            // with a reassembly.
+            //
+            // On right: T0 and T-2 have same length again, but there
+            // isn't an indel as it's ins+del vs del+ins. They're
+            // also the same length as the REF for this region.
+            // Hence likelihood of this variant existing is tied in
+            // with their equal and high similarity with/to the ref.
+            //
+            // We could do an alignment of tcons[0] and tcons[1] and check
+            // whether their differences are consistent with (ie the
+            // hamming distance is at least ABS(types[t]/2).  I don't think
+            // it'll rescue many FPs though.
+
+#ifdef CONS_DEBUG
+            {
+                int j;
+                for (j = 0; j < 2; j++) {
+                    int k;
+                    fprintf(stderr, "Cons%d @ %d %4d/%4d ",
+                            j, pos, types[t], left_shift);
+                    for (k = 0; k < tcon_len[j]; k++) {
+                        if (k == cpos_pos)
+                            putc('#', stderr);
+                        putc("ACGTN"[(uint8_t)tcons[j][k]], stderr);
+                    }
+                    putc('\n', stderr);
+                }
+            }
+#endif
+
+            // Scan for base-runs in the insertion.
+            // We use this to avoid over-correction in est_seqQ when the
+            // insertion is not part of the neighbouring homopolymer.
+            int k = tcons[0][cpos_pos], j;
+            for (j = 0; j < types[t]; j++)
+                if (tcons[0][cpos_pos+j] != k)
+                    break;
+            if (j && j == types[t])
+                l_run_ins |= "\x1\x2\x4\x8\xf"[k]; // ACGTN
+            if (types[t] < 0)
+                l_run_ins |= 0xff;
+
+            // align each read to consensus(es)
+            for (i = 0; i < n_plp[s]; ++i, ++K) {
+                bam_pileup1_t *p = plp[s] + i;
+
+                // Some basic ref vs alt stats.
+                int imq = p->b->core.qual > 59 ? 59 : p->b->core.qual;
+                imq *= nqual_over_60;
+
+                int sc_len, slen, epos, sc_end;
+
+                // Only need to gather stats on one type, as it's
+                // identical calculation for all the subsequent ones
+                // and we're sharing the same stats array
+                if (t == 0) {
+                    // Gather stats for INFO field to aid filtering.
+                    // mq and sc_len not very helpful for filtering, but could
+                    // help in assigning a better QUAL value.
+                    //
+                    // Pos is slightly useful.
+                    // Base qual can be useful, but need qual prior to BAQ?
+                    // May need to cache orig quals in aux tag so we can fetch
+                    // them even after mpileup step.
+                    get_pos(bca, p, &sc_len, &slen, &epos, &sc_end);
+
+                    assert(imq >= 0 && imq < bca->nqual);
+                    assert(epos >= 0 && epos < bca->npos);
+                    assert(sc_len >= 0 && sc_len < 100);
+                    if (p->indel) {
+                        bca->ialt_mq[imq]++;
+                        bca->ialt_scl[sc_len]++;
+                        bca->ialt_pos[epos]++;
+                    } else {
+                        bca->iref_mq[imq]++;
+                        bca->iref_scl[sc_len]++;
+                        bca->iref_pos[epos]++;
+                    }
+                }
+
+                int qbeg, qpos, qend, tbeg, tend, kk;
+                uint8_t *seq = bam_get_seq(p->b);
+                uint32_t *cigar = bam_get_cigar(p->b);
+                if (p->b->core.flag & BAM_FUNMAP) continue;
+
+                // FIXME: the following loop should be better moved outside;
+                // nonetheless, realignment should be much slower anyway.
+                for (kk = 0; kk < p->b->core.n_cigar; ++kk)
+                    if ((cigar[kk]&BAM_CIGAR_MASK) == BAM_CREF_SKIP)
+                        break;
+                if (kk < p->b->core.n_cigar)
+                    continue;
+
+                // determine the start and end of sequences for alignment
+                int left2 = left, right2 = right;
+                int min_win_size = MAX(-biggest_del, biggest_ins);
+                min_win_size += ABS(left_shift) + ABS(right_shift);
+                {
+                    rep_ele *reps, *elt, *tmp;
+                    reps = find_STR(tcons[0], tcon_len[0], 0);
+                    //int max_str = 0;
+                    int tot_str = 0;
+                    DL_FOREACH_SAFE(reps, elt, tmp) {
+                        // if (max_str < elt->end - elt->start)
+                        //     max_str = elt->end - elt->start;
+                        tot_str += elt->end - elt->start;
+                        DL_DELETE(reps, elt);
+                        free(elt);
+                    }
+
+                    // Ideally max_str should be enough, but it's still not
+                    // sufficient in longer range some repeats.
+                    //min_win_size += max_str;
+                    min_win_size += tot_str;
+                }
+                min_win_size += 10;
+
+// TEST 8
+                if (p->b->core.l_qseq > 1000) {
+                    // long read data needs less context.  It also tends to
+                    // have many more candidate indels to investigate so
+                    // speed here matters more.
+                    if (pos - left >= min_win_size)
+                        left2 = MAX(left2, pos - min_win_size);
+                    if (right-pos >= min_win_size)
+                        right2 = MIN(right2, pos + min_win_size);
+                }
+
+                // Genomic coords for first and last base of query
+                // alignment.  This is only used in bcf_cgp_align_score
+                // for computing scores by looking for the proximity
+                // of STRs with the end of the query alignment.
+                int r_start = p->b->core.pos;
+                int r_end = bam_cigar2rlen(p->b->core.n_cigar,
+                                           bam_get_cigar(p->b));
+                r_end += -1 + r_start;
+
+
+                // Map left2/right2 genomic coordinates to qbeg/qend
+                // query coordinates.  The query may not span the
+                // entire left/right region, so this also returns the
+                // equivalent genomic coords for qbeg/qend in tbeg/tend.
+                qbeg = tpos2qpos(&p->b->core, bam_get_cigar(p->b),
+                                 left2, 0, &tbeg);
+                qpos = tpos2qpos(&p->b->core, bam_get_cigar(p->b), pos,
+                                     0, &tend) - qbeg;
+                qend = tpos2qpos(&p->b->core, bam_get_cigar(p->b),
+                                 right2, 1, &tend);
+
+                int old_tend = tend;
+                int old_tbeg = tbeg;
+
+                // write the query sequence
+                for (l = qbeg; l < qend; ++l)
+                    query[l - qbeg] = seq_nt16_int[bam_seqi(seq, l)];
+
+                // tbeg and tend are the genomic locations equivalent
+                // to qbeg and qend on the sequence.
+                // These may being entirely within our left/right
+                // coordinates over which we've computed the
+                // consensus, or overlapping to left/right.
+                //
+                // We know an estimation of band, plus biggest indel,
+                // so we can trim tbeg/tend to a smaller region if we
+                // wish here.  This speeds up BAQ scoring.
+                int wband = band + MAX(-biggest_del, biggest_ins)*2 + 20;
+                int tend1 = left + tcon_len[0] - (left2-left);
+                int tend2 = left + tcon_len[1] - (left2-left);
+                tend1 = MIN(tend1, old_tend + wband);
+                tend2 = MIN(tend2, old_tend + wband);
+                tbeg = MAX(left2, old_tbeg - wband);
+
+                // do realignment; this is the bottleneck.
+                //
+                // Note low score = good, high score = bad.
+                if (tend1 > tbeg && tend2 > tbeg) {
+                    //fprintf(stderr, "Num %d\n", i);
+                    if (bcf_cgp_align_score(p, bca, types[t], band,
+                                            (uint8_t *)tcons[0] + left2-left,
+                                            (uint8_t *)tcons[1] + left2-left,
+                                            (uint8_t *)query,
+                                            r_start, r_end,
+                                            tbeg, tend1, tend2,
+                                            left2, left + tcon_len[0],
+                                            qbeg, qend, pos,qpos, -biggest_del,
+                                            qavg, bca->del_bias,
+                                            &score[K*n_types + t],
+                                            &str_len1, &str_len2) < 0) {
+                        goto err;
+                    }
+#ifdef ALIGN_DEBUG
+                    fprintf(stderr, "type %d %x / %x\t%s\n",
+                            types[t],
+                            score[K*n_types + t] >> 8,
+                            score[K*n_types + t] & 0xff,
+                            bam_get_qname(p->b));
+#endif
+                } else {
+                    // place holder large cost for reads that cover the
+                    // region entirely within a deletion (thus tend < tbeg).
+                    score[K*n_types + t] = 0xffffff;
+                }
+            }
+            free(tcons);
+        }
+    }
+
+    // compute indelQ
+    if (!(l_run_base & l_run_ins))
+        l_run = 1; // different base type in ins to flanking region.
+    n_alt = bcf_cgp_compute_indelQ(n, n_plp, plp, bca, inscns, l_run, max_ins,
+                                   ref_type, types, n_types, qavg, score,
+                                   str_len1, str_len2);
+
+ err:
+    // free
+    free(query);
+    free(score);
+    free(types);
+    free(inscns);
+
+    return n_alt > 0? 0 : -1;
+}
diff --git a/bcftools/bam2bcf_edlib.c.pysam.c b/bcftools/bam2bcf_edlib.c.pysam.c
new file mode 100644 (file)
index 0000000..fa009ff
--- /dev/null
@@ -0,0 +1,1706 @@
+#include "bcftools.pysam.h"
+
+/*  bam2bcf_indel.c -- indel caller.
+
+    Copyright (C) 2010, 2011 Broad Institute.
+    Copyright (C) 2012-2014,2016-2017, 2021-2024 Genome Research Ltd.
+
+    Author: Heng Li <lh3@sanger.ac.uk>
+            Petr Danecek <pd3@sanger.ac.uk>
+           James Bonfield <jkb@sanger.ac.uk>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.  */
+
+// Show consensus
+//#define CONS_DEBUG
+
+// Show alignments to consensus
+//#define ALIGN_DEBUG
+
+#include <assert.h>
+#include <ctype.h>
+#include <string.h>
+#include <math.h>
+#include <htslib/hts.h>
+#include <htslib/sam.h>
+#include <htslib/khash_str2int.h>
+#include "bam2bcf.h"
+#include "str_finder.h"
+
+#include <htslib/ksort.h>
+// Is there no way to share these between the 3 implementations?
+KSORT_INIT_STATIC_GENERIC(uint32_t)
+
+#define MINUS_CONST 0x10000000
+
+#define MAX_TYPES 64
+
+#ifndef MIN
+#  define MIN(a,b) ((a)<(b)?(a):(b))
+#endif
+
+#ifndef ABS
+#  define ABS(a) ((a)<0?-(a):(a))
+#endif
+
+#ifndef MAX
+#  define MAX(a,b) ((a)>(b)?(a):(b))
+#endif
+
+// l is the relative gap length and l_run is the length of the homopolymer
+// on the reference.
+//
+// Larger seqQ is good, so increasing tandemQ calls more indels,
+// and longer l_run means fewer calls.  It is capped later at 255.
+// For short l_runs, the qual is simply based on size of indel
+// larger ones being considered more likely to be real.
+// Longer indels get assigned a score based on the relative indel size
+// to homopolymer, where l_run base will have already been verified by
+// the caller to ensure it's compatible.
+static inline int est_seqQ(const bcf_callaux_t *bca, int l, int l_run, int str_len)
+{
+    int q, qh;
+    // Short indels are more likely sequencing error than large ones.
+    // So "seqQ" scales with size of observation "l".
+    //
+    // Note openQ and extQ are error likelihoods in Phred scale.  Hence high
+    // openQ means we're very unlikely to miscall an indel.
+    // Ie it's not the open/ext "costs" normally used in alignment; more the reverse.
+    //
+    // We use MIN(q,qh) below, so we can remove the q component by specifying
+    // a large -o parameter in mpileup.
+    q = bca->openQ + bca->extQ * (abs(l) - 1);
+
+    // Orig method; best with Illumina (high openQ)
+//    qh = bca->tandemQ * (double)abs(l) / l_run + .499;
+
+    // Penalise longer homopolymers quadratically more, but boost shorter ones.
+    // Best with CCS (low openQ)
+    //qh = 2 * bca->tandemQ * pow((double)abs(l) / l_run, 1.5) + .499;
+
+    // (l/l_run)^1.26 for openQ=25 or ^1 for openQ=40.
+//    double openQ = MIN(40, bca->openQ);
+//    qh = (30/openQ) * bca->tandemQ
+//        * pow((double)abs(l) / l_run, 1/sqrt(openQ/40)) + .499;
+
+    // Linear scaled on openQ too
+    qh = bca->tandemQ * (double)abs(l) / l_run + .499;
+
+    // Generic maybe ?
+    // power = 1/sqrt(MIN(40,bca->openQ)/40.);
+    // qh = ... * pow((double)abs(l)/l_run, power)
+
+    // bam2bcf.c caps has "if q>seqQ) q=seqQ" so it caps base qual 'q'.
+    // A 1bp indel would therefore have a maximum qual it could be considered based
+    // on open+ext.  Hence why openQ is phred score indicating if the base is real
+    // or an over/under-call. (high openQ means high trust in base)
+    return q < qh? q : qh;
+}
+
+// Part of bcf_call_gap_prep.
+//
+// Scans the pileup to identify all the different sizes of indels
+// present.
+// types[] returned is sorted by size, from smallest (maybe negative) to largest.
+//
+// Returns types and fills out n_types_r,  max_rd_len_r and ref_type_r,
+//         or NULL on error.
+static int *bcf_cgp_find_types(int n, int *n_plp, bam_pileup1_t **plp,
+                               int pos, bcf_callaux_t *bca, const char *ref,
+                               int *max_rd_len_r, int *n_types_r,
+                               int *ref_type_r, int *N_r) {
+    int i, j, t, s, N, m, max_rd_len, n_types;
+    int n_alt = 0, n_tot = 0, indel_support_ok = 0;
+    uint32_t *aux;
+    int *types;
+
+    // N is the total number of reads
+    for (s = N = 0; s < n; ++s)
+        N += n_plp[s];
+
+    bca->max_support = bca->max_frac = 0;
+    aux = (uint32_t*) calloc(N + 1, 4);
+    if (!aux)
+        return NULL;
+
+    m = max_rd_len = 0;
+    aux[m++] = MINUS_CONST; // zero indel is always a type (REF)
+
+    // Fill out aux[] array with all the non-zero indel sizes.
+    // Also tally number with indels (n_alt) and total (n_tot).
+    for (s = 0; s < n; ++s) {
+        int na = 0, nt = 0;
+        for (i = 0; i < n_plp[s]; ++i) {
+            const bam_pileup1_t *p = plp[s] + i;
+            ++nt;
+            if (p->indel != 0) {
+                ++na;
+                aux[m++] = MINUS_CONST + p->indel;
+            }
+
+            // FIXME: cache me in pileup struct.
+            j = bam_cigar2qlen(p->b->core.n_cigar, bam_get_cigar(p->b));
+            if (j > max_rd_len) max_rd_len = j;
+        }
+        double frac = (double)na/nt;
+        if ( !indel_support_ok && na >= bca->min_support
+             && frac >= bca->min_frac )
+            indel_support_ok = 1;
+        if ( na > bca->max_support && frac > 0 )
+            bca->max_support = na, bca->max_frac = frac;
+
+        n_alt += na;
+        n_tot += nt;
+    }
+
+    // Sort aux[] and dedup
+    ks_introsort(uint32_t, m, aux);
+    for (i = 1, n_types = 1; i < m; ++i)
+        if (aux[i] != aux[i-1]) ++n_types;
+
+    // Taking totals makes it hard to call rare indels (IMF filter)
+    if ( !bca->per_sample_flt )
+        indel_support_ok = ( (double)n_alt / n_tot < bca->min_frac
+                             || n_alt < bca->min_support )
+            ? 0 : 1;
+    if ( n_types == 1 || !indel_support_ok ) { // then skip
+        free(aux);
+        return NULL;
+    }
+
+    // Bail out if we have far too many types of indel
+    if (n_types >= MAX_TYPES) {
+        free(aux);
+        // TODO revisit how/whether to control printing this warning
+        if (hts_verbose >= 2)
+            fprintf(bcftools_stderr, "[%s] excessive INDEL alleles at position %d. "
+                    "Skip the position.\n", __func__, pos + 1);
+        return NULL;
+    }
+
+    // To prevent long stretches of N's to be mistaken for indels
+    // (sometimes thousands of bases), check the number of N's in the
+    // sequence and skip places where half or more reference bases are Ns.
+    int nN=0, i_end = pos + (2*bca->indel_win_size < max_rd_len
+                            ?2*bca->indel_win_size : max_rd_len);
+    for (i=pos; i<i_end && ref[i]; i++)
+        nN += ref[i] == 'N';
+    if ( nN*2>(i-pos) ) {
+        free(aux);
+        return NULL;
+    }
+
+    // Finally fill out the types[] array detailing the size of insertion
+    // or deletion.
+    types = (int*)calloc(n_types, sizeof(int));
+    if (!types) {
+        free(aux);
+        return NULL;
+    }
+    t = 0;
+    for (i = 0; i < m; ++i) {
+        int sz = (int32_t)(aux[i] - MINUS_CONST);
+        int j;
+        for (j = i+1; j < m; j++)
+            if (aux[j] != aux[i])
+                break;
+
+        if (sz == 0
+            || (j-i >= bca->min_support &&
+                // Note, doesn't handle bca->per_sample_flt yet
+                (bca->per_sample_flt
+                 || (double)(j-i) / n_tot >= bca->min_frac)))
+            types[t++] = sz;
+        i = j-1;
+    }
+    free(aux);
+
+    if (t <= 1) {
+        free(types);
+        return NULL;
+    }
+    n_types = t;
+
+    // Find reference type; types[?] == 0)
+    for (t = 0; t < n_types; ++t)
+        if (types[t] == 0) break;
+
+    *ref_type_r   = t;
+    *n_types_r    = n_types;
+    *max_rd_len_r = max_rd_len;
+    *N_r          = N;
+
+    return types;
+}
+
+// Increment ins["str"] and freq["str"]
+#define NI 100 // number of alternative insertion sequences
+// Could use a hash table too, but expectation is a tiny number of alternatives
+typedef struct {
+    char *str[NI];
+    int len[NI];
+    int freq[NI];
+} str_freq;
+
+static int bcf_cgp_append_cons(str_freq *sf, char *str, int len, int freq) {
+    int j;
+
+    for (j = 0; j < NI && sf->str[j]; j++) {
+        if (sf->len[j] == len && memcmp(sf->str[j], str, len) == 0)
+            break;
+    }
+    if (j >= NI)
+        return 0; // too many choices; discard
+
+    sf->freq[j]+=freq;
+    if (!sf->str[j]) {
+        // new insertion
+        if (!(sf->str[j] = malloc(len+1)))
+            return -1;
+        memcpy(sf->str[j], str, len);
+        sf->len[j] = len;
+    }
+
+    return 0;
+}
+
+/*
+ * Compute the consensus for a specific indel type at pos.
+ *
+ * left_shift is the number of inserted(+) or deleted(-) bases added to
+ * the consensus before we get to pos.  This is necessary so the alignment
+ * band is correct as it's expected to start at left/right edges in
+ * sync
+ *
+ * We accumulate into several buffers for counting base types:
+ * cons_base   - consensus of data with p->indel == type, bases or gap
+ * ref_base    - consensus of data with p->indel != type, bases or gap
+ * cons_ins    - consensus of data with p->indel == type, insertions
+ * ref_ins     - consensus of data with p->indel == type, bases or gap
+ *
+ * The purpose of cons_ins vs cons_base is if we have very low
+ * coverage due to nearly all reads being another type, then we can
+ * still get a robust consensus using the other data.  If we don't
+ * have shallow data, then we'll not use as much of ref_base as we may
+ * have correlated variants.
+ *
+ * Eg:
+ * REF: AGCTATGAGGCTGATA
+ * SEQ: AGGTAGGAGGGTGATA (x1)
+ * SEQ: AGCTACGAGG*TGATA (x24)
+ * SEQ: AGCTACTAGG*TGATA (x24)
+ *
+ * Cons for no-del is Cs not Gs.  Cannot trust it, so use N if shallow.
+ * CON: AGCTACNAGGGTGATA
+ *
+ * There are still some problems in cons_ins vs ref_ins assignment.
+ * We sometimes seem multiple similar-length insertions added at
+ * different locations.  Ideally we'd like to consider these as all
+ * the same insertion if the size is the same and it's comparable seq.
+ */
+#define MAX_INS 8192
+static char **bcf_cgp_consensus(int n, int *n_plp, bam_pileup1_t **plp,
+                                int pos, bcf_callaux_t *bca, const char *ref,
+                                int ref_len, int left, int right,
+                                int sample, int type, int biggest_del,
+                                int *left_shift, int *right_shift,
+                                int *band, int *tcon_len, int *cpos_pos,
+                                int pos_l, int pos_r) {
+    // Map ASCII ACGTN* to 012345
+    static uint8_t base6[256] = {
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        4,4,4,4,4,4,4,4,  4,4,5,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        //A   C       G       *^                     T
+        4,0,4,1,4,4,4,2,  4,4,4,4,4,4,4,4,  4,4,4,4,3,3,4,4,  4,4,4,4,4,4,4,4,
+        4,0,4,1,4,4,4,2,  4,4,4,4,4,4,4,4,  4,4,4,4,3,3,4,4,  4,4,4,4,4,4,4,4,
+
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+        4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,  4,4,4,4,4,4,4,4,
+    };
+
+    // single base or del
+    int (*cons_base)[6] = calloc(right - left + 1, sizeof(*cons_base));
+    // multi-base insertions
+    str_freq *cons_ins  = calloc(right - left + 1, sizeof(*cons_ins));
+
+    // non-indel ref for all reads on this sample, rather than those just
+    // matching type.  We use this for handling the case where we have a
+    // homozygous deletion being studied, but with 1 or 2 reads misaligned
+    // and containing a base there.
+    //
+    // Eg if the type[]=0 consensus is made up of a very small sample size,
+    // which is also enriched for highly error prone data.  We can use
+    // the other reads from type[] != 0 to flesh out the consensus and
+    // improve accuracy.
+    int (*ref_base)[6]  = calloc(right - left + 1, sizeof(*ref_base));
+    str_freq *ref_ins   = calloc(right - left + 1, sizeof(*ref_ins));
+    int i, j, k, s = sample;
+    char **cons = NULL;
+
+    if (!cons_base || !cons_ins || !ref_base || !ref_ins)
+        goto err;
+
+    //--------------------------------------------------
+    // Accumulate sequences into cons_base and cons_ins arrays
+    int local_band_max = 0; // maximum absolute deviation from diagonal
+    int total_span_str = 0;
+    int type_depth = 0;
+    for (i = 0; i < n_plp[s]; i++) {
+        const bam_pileup1_t *p = plp[s] + i;
+        bam1_t *b = p->b;
+        int x = b->core.pos;  // ref coordinate
+        int y = 0;            // seq coordinate
+        uint32_t *cigar = bam_get_cigar(b);
+        uint8_t *seq = bam_get_seq(b);
+
+        int local_band = 0; // current deviation from diagonal
+        for (k = 0; k < b->core.n_cigar; ++k) {
+            int op  = cigar[k] &  BAM_CIGAR_MASK;
+            int len = cigar[k] >> BAM_CIGAR_SHIFT;
+            int base;
+            int skip_to = 0;
+
+            switch(op) {
+            case BAM_CSOFT_CLIP:
+                y += len;
+                break;
+
+            case BAM_CMATCH:
+            case BAM_CEQUAL:
+            case BAM_CDIFF: {
+                // Can short-cut this with j_start and j_end based on
+                // x+len and left,right
+                for (j = 0; j < len; j++, x++, y++) {
+                    if (x < left) continue;
+                    if (x >= right) break;
+
+                    base = bam_seqi(seq, y);
+                    if (p->indel == type)
+                        // Convert 4-bit base ambig code to 0,1,2,3,4 range
+                        cons_base[x-left][seq_nt16_int[base]]++;
+                    else if (x != pos+1) // indel being assessed question
+                        ref_base[x-left][seq_nt16_int[base]]++;
+                }
+                break;
+            }
+
+            case BAM_CINS: {
+                if (x >= left && x < right) {
+                    local_band += p->indel;
+                    if (local_band_max < local_band)
+                        local_band_max = local_band;
+                }
+
+                char ins[MAX_INS];
+                for (j = 0; j < len; j++, y++) {
+                    if (x < left) continue;
+                    if (x >= right)
+                        break;
+                    base = bam_seqi(seq, y);
+                    if (j < MAX_INS)
+                        ins[j] = seq_nt16_int[base];
+                }
+
+                // Insertions come before a ref match.
+                // 5I 5M is IIIIIM M M M M events, not
+                // {IIIII,M} M M M M choice.  So we need to include the
+                // next match in our sequence when choosing the consensus.
+                if (x >= left && x < right) {
+                    int ilen = j<MAX_INS?j:MAX_INS;
+                    if (p->indel == type /*&& x == pos+1*/) {
+                        // Assume any ins of the same size is the same ins.
+                        // (This rescues misaligned insertions.)
+                        if (bcf_cgp_append_cons(&cons_ins[x-left], ins,
+                                                ilen, 1) < 0)
+                            goto err;
+                        type_depth += (x == pos+1);
+                    } else  if (x != pos+1){
+                        if (bcf_cgp_append_cons(&ref_ins[x-left],  ins,
+                                                ilen, 1) < 0)
+                            goto err;
+                    }
+                }
+                break;
+            }
+
+            case BAM_CDEL:
+                if (x >= left && x < right) {
+                    local_band += p->indel;
+                    if (local_band_max < -local_band)
+                        local_band_max = -local_band;
+                }
+
+                // Maybe not perfect for I/D combos, but likely sufficient.
+                for (j = 0; j < len; j++, x++) {
+                    if (x < left) continue;
+                    if (x >= right) break;
+                    if ((p->indel == type && !p->is_del) ||  // starts here
+                        (p->indel == 0 && p->is_del && len == -type)) { // left
+                        cons_base[x-left][5]++;
+                        type_depth += (x == pos+1);
+                    } else if (x+len <= pos+1 || (skip_to && x > skip_to))
+                        ref_base[x-left][5]++;
+                    else if (x <= pos && x+len > pos+1) {
+                        // we have a deletion which overlaps pos, but
+                        // isn't the same "type".  We don't wish to
+                        // include these as they may bias the
+                        // evaluation by confirming against a
+                        // secondary consensus produced with the other
+                        // deletion.  We set a marker for how long to
+                        // skip adding to ref_base.
+                        if (x > skip_to)
+                            skip_to = x+len;
+                    }
+                }
+                break;
+            }
+        }
+
+        if (b->core.pos <= pos_l && x >= pos_r)
+            total_span_str++;
+
+        // Also track the biggest deviation +/- from diagonal.  We use
+        // this band observation in our BAQ alignment step.
+        if (*band < local_band_max)
+            *band = local_band_max;
+    }
+
+    //--------------------------------------------------
+    // Expand cons_base to include depth from ref_base/ref_ins
+    // Caveat: except at pos itself, where true ref is used if type != 0
+
+#if 1 // TEST 1
+    // We could retest this heuristic further maybe.
+    for (i = 0; i < right-left; i++) {
+        // Total observed depth
+        int t = cons_base[i][0] + cons_base[i][1] + cons_base[i][2] +
+            cons_base[i][3] + cons_base[i][4] + cons_base[i][5];
+        for (j = 0; j < NI; j++) {
+            if (!cons_ins[i].str[j])
+                break;
+            t += cons_ins[i].freq[j];
+        }
+
+        // Similarly for depth on the non-ALT calls (NB: not necessarily
+        // REF as maybe it's other ALTs).
+        int r = ref_base[i][0] + ref_base[i][1] + ref_base[i][2] +
+            ref_base[i][3] + ref_base[i][4] + ref_base[i][5];
+        for (j = 0; j < NI; j++) {
+            if (!ref_ins[i].str[j])
+                break;
+            r += ref_ins[i].freq[j];
+        }
+
+        // When evaluating this particular indel, we don't want to
+        // penalise alignments by SNP errors elsewhere.  This can
+        // happen when we have low depth for a particular 'type'.
+        //
+        // So add in a little data from ref_base/ref_ins.
+        double rfract = (r - t*2)*.75 / (r+1);
+
+        if (rfract < 1.01 / (r+1e-10))
+            rfract = 1.01 / (r+1e-10); // low depth compensation
+//        if (rfract > 0.2)
+//            rfract = 0.2;
+
+        // TODO: consider limiting rfract so we never drown out the
+        // signal.  We want to use the remaining data only to correct
+        // for sequencing errors in low depth alleles.  If we get
+        // conflicts, it's better to use N than to change a base
+        // incase that variant is genuine.
+        if (i+left >= pos+1 && i+left < pos+1-biggest_del) {
+            // We're overlapping the current indel region, so
+            // we don't wish to bring in evidence from the other
+            // "type" data as it'll harm calling.
+            continue;
+        } else {
+            // Otherwise add in a portion of other data to
+            // boost low population numbers.
+            cons_base[i][0] += rfract * ref_base[i][0];
+            cons_base[i][1] += rfract * ref_base[i][1];
+            cons_base[i][2] += rfract * ref_base[i][2];
+            cons_base[i][3] += rfract * ref_base[i][3];
+            cons_base[i][4] += rfract * ref_base[i][4];
+            cons_base[i][5] += rfract * ref_base[i][5];
+        }
+
+        // Similarly for insertions too; consider a different rfract here?
+        for (j = 0; j < NI; j++) {
+            if (!ref_ins[i].str[j])
+                break;
+            if (bcf_cgp_append_cons(&cons_ins[i],
+                                    ref_ins[i].str[j], ref_ins[i].len[j],
+                                    rfract * ref_ins[i].freq[j]) < 0)
+                goto err;
+        }
+    }
+#endif
+
+    //--------------------------------------------------
+    // Allocate consensus buffer, to worst case length
+    int max_len = right-left;
+    for (i = 0; i < right-left; i++) {
+        if (!cons_ins[i].str[0])
+            continue;
+
+        int ins = 0;
+        for (j = 0; j < NI; j++) {
+            if (!cons_ins[i].str[j])
+                break;
+            if (cons_ins[i].str[j] && ins < cons_ins[i].len[j])
+                ins = cons_ins[i].len[j];
+        }
+        max_len += ins;
+    }
+    max_len += MAX(0, type); // incase type inserted bases never occur
+    cons = malloc((max_len+1)*2 + sizeof(char *)*2);
+    if (!cons)
+        goto err;
+    cons[0] = (char *)&cons[2];
+    cons[1] = cons[0] + max_len+1;
+
+    //--------------------------------------------------
+    // Merge insertions where they are the same length but different
+    // sequences.
+    // NB: we could just index by length and have accumulators for each,
+    // instead of storing separately and merging later (here).
+    // Ie str_freq.str is [NI][5] instead.
+    for (i = 0; i < right-left; i++) {
+        int ins[MAX_INS][5];
+        for (j = 0; j < NI; j++) {
+            if (!cons_ins[i].str[j])
+                break;
+
+            if (cons_ins[i].freq[j] == 0)
+                continue; // already merged
+
+            int l;
+            for (l = 0; l < cons_ins[i].len[j]; l++) {
+                // Append to relevant frequency counter, zero all others
+                ins[l][0] = ins[l][1] = ins[l][2] = ins[l][3] = ins[l][4] = 0;
+                uint8_t b = cons_ins[i].str[j][l];
+                ins[l][b] = cons_ins[i].freq[j];
+            }
+
+            // Merge other insertions of the same length to ins[] counters
+            for (k = j+1; k < NI; k++) {
+                if (!cons_ins[i].str[k])
+                    break;
+                if (cons_ins[i].len[k] != cons_ins[i].len[j])
+                    continue;
+                if (cons_ins[i].freq[k] == 0)
+                    continue; // redundant?
+
+                // Merge str[j] and str[k]
+                for (l = 0; l < cons_ins[i].len[k]; l++) {
+                    uint8_t b = cons_ins[i].str[k][l];
+                    ins[l][b] += cons_ins[i].freq[k];
+                }
+                cons_ins[i].freq[j] += cons_ins[i].freq[k];
+                cons_ins[i].freq[k] = 0;
+            }
+
+            // Now replace ins[j] with the consensus insertion of this len.
+            for (l = 0; l < cons_ins[i].len[j]; l++) {
+                int max_v = 0, base = 0;
+                int tot = ins[l][0] + ins[l][1] + ins[l][2]
+                        + ins[l][3] + ins[l][4];
+                if (max_v < ins[l][0]) max_v = ins[l][0], base = 0;
+                if (max_v < ins[l][1]) max_v = ins[l][1], base = 1;
+                if (max_v < ins[l][2]) max_v = ins[l][2], base = 2;
+                if (max_v < ins[l][3]) max_v = ins[l][3], base = 3;
+                if (max_v < ins[l][4]) max_v = ins[l][4], base = 4;
+
+                cons_ins[i].str[j][l] = (max_v > 0.6*tot) ? base : 4;
+            }
+        }
+    }
+
+#define CONS_CUTOFF      .40 // % needed for base vs N
+#define CONS_CUTOFF_DEL  .35 // % to include any het del
+#define CONS_CUTOFF2     .80 // % needed for gap in cons[1]
+#define CONS_CUTOFF_INC  .35 // % to include any insertion cons[0]
+#define CONS_CUTOFF_INC2 .80 // % to include any insertion cons[1] HOM
+#define CONS_CUTOFF_INS  .60 // and then 60% needed for it to be bases vs N
+
+    //--------------------------------------------------
+    // Walk through the frequency arrays to call the consensus.
+    // We produce cons[0] and cons[1].  Both include strongly
+    // homozygous indels.  Both also include the indel at 'pos'.
+    // However for heterozygous indels we call the most likely event
+    // for cons[0] and the less-likely alternative in cons[1].
+    // TODO: a proper phase analysis so multiple events end up
+    // combining together into the correct consensus.
+    *left_shift = 0;
+    *right_shift = 0;
+    int cnum;
+
+    // Het call filled out in cnum==0 (+ve or -ve).
+    // Used in cnum==1 to do the opposite of whichever way we did before.
+    int heti[MAX_INS] = {0}, hetd[MAX_INS] = {0};
+
+    *cpos_pos = -1;
+    for (cnum = 0; cnum < 2; cnum++) {
+        for (i = k = 0; i < right-left; i++) {
+            // Location in consensus matching the indel itself
+            if (i >= pos-left+1 && *cpos_pos == -1)
+                *cpos_pos = k;
+
+            int max_v = 0, max_v2 = 0, max_j = 4, max_j2 = 4, tot = 0;
+            for (j = 0; j < 6; j++) {
+                // Top 2 consensus calls
+                if (max_v < cons_base[i][j]) {
+                    max_v2 = max_v, max_j2 = max_j;
+                    max_v = cons_base[i][j], max_j = j;
+                } else if (max_v2 < cons_base[i][j]) {
+                    max_v2 = cons_base[i][j], max_j2 = j;
+                }
+                tot += cons_base[i][j];
+            }
+
+            // +INS
+            int max_v_ins = 0, max_j_ins = 0;
+            int tot_ins = 0;
+            for (j = 0; j < NI; j++) {
+                if (i+left==pos+1)
+                if (type > 0 && i+left == pos+1
+                    && cons_ins[i].len[j] < type && j == 0) {
+                    cons_ins[i].str[j] = realloc(cons_ins[i].str[j], type);
+                    if (!cons_ins[i].str[j])
+                        goto err;
+                    memset(cons_ins[i].str[j] + cons_ins[i].len[j],
+                           4, type - cons_ins[i].len[j]);
+                    cons_ins[i].len[j] = type;
+                }
+                if (!cons_ins[i].str[j])
+                    break;
+                if (cons_ins[i].freq[j] == 0)
+                    continue; // previously merged
+
+                if (max_v_ins < cons_ins[i].freq[j])
+                    //if (i != pos-left+1 || cons_ins[i].len[j] == type)
+                    max_v_ins = cons_ins[i].freq[j], max_j_ins = j;
+                tot_ins += cons_ins[i].freq[j];
+            }
+
+            // NB: tot is based on next matching base, so it includes
+            // everything with or without the insertion.
+            int tot_sum = tot;
+            int always_ins =
+                (i == pos-left+1 && type>0) ||       // current eval
+                max_v_ins > CONS_CUTOFF_INC2*tot_sum;// HOM
+            int het_ins = 0;
+            if (!always_ins && max_v_ins >= bca->min_support) {
+                // Candidate HET ins.
+                if (cnum == 0) {
+                    het_ins = max_v_ins > CONS_CUTOFF_INC * tot_sum;
+                    if (i < MAX_INS) heti[i] = het_ins
+                                      ? 1
+                                      : (max_v_ins > .3*tot_sum ? -1:0);
+                } else {
+                    // HET but uncalled before
+                    het_ins = i < MAX_INS ? (heti[i] == -1) : 0;
+                }
+            }
+
+            if (always_ins || het_ins) {
+                if (max_v_ins > CONS_CUTOFF_INS*tot_ins) {
+                    // Insert bases
+                    for (j = 0; j < cons_ins[i].len[max_j_ins]; j++) {
+                        if (cnum == 0) {
+                            if (k < pos-left+*left_shift)
+                                (*left_shift)++;
+                            else
+                                (*right_shift)++;
+                        }
+                        cons[cnum][k++] = cons_ins[i].str[max_j_ins][j];
+                    }
+                } else {
+                    for (j = 0; j < cons_ins[i].len[max_j_ins]; j++)
+                        cons[cnum][k++] = 4; // 'N';
+                }
+            }
+
+            // Call deletions & bases
+            int always_del = (type < 0 && i > pos-left && i <= pos-left-type)
+                || cons_base[i][5] > CONS_CUTOFF2 * tot; // HOM del
+            int het_del = 0;
+            if (!always_del && cons_base[i][5] >= bca->min_support) {
+                // Candidate HET del.
+                if (cnum == 0) {
+                    int tot2 = tot;
+                    if (i > pos-left && i <= pos-left-biggest_del)
+                        tot2 = total_span_str - type_depth;
+                    het_del = cons_base[i][5] >= CONS_CUTOFF_DEL * tot2;
+
+                    if (i < MAX_INS) {
+                        if (i > pos-left && i <= pos-left-biggest_del)
+                            hetd[i] = 0;
+                        else
+                            hetd[i] = het_del
+                                ? 1
+                                : (cons_base[i][5] >= .3 * tot2 ? -1 : 0);
+                    }
+                } else {
+                    // HET del uncalled on cnum 0
+                    het_del = i < MAX_INS ? (hetd[i] == -1) : 0;
+                    if (max_j == 5 && het_del == 0) {
+                        max_v = max_v2;
+                        max_j = max_j2;
+                    }
+                }
+            }
+            if (always_del || het_del) {
+                // Deletion
+                if (k < pos-left+*left_shift)
+                    (*left_shift)--;
+                else
+                    (*right_shift)++;
+            } else {
+                // Finally the easy case - a non-indel base or an N
+                if (max_v > CONS_CUTOFF*tot)
+                    cons[cnum][k++] = max_j; // "ACGTN*"
+                else if (max_v > 0)
+                    cons[cnum][k++] = 4;     // 'N';
+                else {
+                    cons[cnum][k] = left+k < ref_len
+                        ? base6[(uint8_t)ref[left+k]]
+                        : 4;
+                    k++;
+                }
+            }
+        }
+
+        tcon_len[cnum] = k;
+    }
+
+    // TODO: replace by io_lib's string pool for rapid tidying.
+    // For now this isn't the bottleneck though.
+    for (i = 0; i < right-left; i++) {
+        for (j = 0; j < NI; j++) {
+            if (cons_ins[i].str[j])
+                free(cons_ins[i].str[j]);
+            if (ref_ins[i].str[j])
+                free(ref_ins[i].str[j]);
+        }
+    }
+
+ err:
+    free(cons_base);
+    free(ref_base);
+    free(cons_ins);
+    free(ref_ins);
+
+    return cons;
+}
+
+// A rename of bcf_cgp_calc_cons from bam2bcf_indel.c
+//
+// Compute the insertion consensus for this sample 's' via a basic
+// majority rule.
+//
+// TODO: merge this into bcf_cgp_consensus as another return value?
+static char *bcf_cgp_calc_ins_cons(int n, int *n_plp, bam_pileup1_t **plp,
+                                   int pos, int *types, int n_types,
+                                   int max_ins, int s) {
+    return bcf_cgp_calc_cons(n, n_plp, plp, pos, types, n_types, max_ins, s);
+}
+
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#define MIN(a,b) ((a)<(b)?(a):(b))
+
+// Compile with LIBS="-L. -ldl -ledlib" CLD=g++
+
+// This is faster than ksw and BAQ, meaning we can use larger --indel-size and
+// get a more accurate context, improving alignments further.  This *may*
+// compensate for reduced sensitivity.
+#include "edlib.h"
+int edlib_glocal(uint8_t *ref, int l_ref, uint8_t *query, int l_query,
+                 double m, double del_bias)
+{
+    EdlibAlignConfig cfg = 
+        edlibNewAlignConfig(
+                            //ABS(type)+ABS(l_ref-l_query)+10,
+                            -1, // k; use small positive for faster alignment
+                            EDLIB_MODE_HW, // mode
+#ifdef ALIGN_DEBUG
+                            EDLIB_TASK_PATH,
+#else
+                            EDLIB_TASK_LOC,
+#endif
+                            NULL, // additionalEqualities
+                            0); // additionalEqualitiesLength
+    EdlibAlignResult r = 
+        edlibAlign((char *)query, l_query, (char *)ref, l_ref, cfg);
+
+    if (r.status != EDLIB_STATUS_OK || r.numLocations < 1 ||
+        !r.endLocations || !r.startLocations) {
+        edlibFreeAlignResult(r);
+        return INT_MAX;
+    }
+
+#ifdef ALIGN_DEBUG
+    // NB: Needs linking against the C++ libedlib.a as our cut-down C
+    // implementation misses the alignment generation code.
+    {
+        int i, j = 0, pt = r.startLocations[0], pq = 0;
+        char line1[80];
+        char line2[80];
+        char line3[80];
+        for (i = 0; i < r.alignmentLength && pt < r.endLocations[0]; i++) {
+            int n;
+            switch (n = r.alignment[i]) {
+            case 0: // match
+            case 3: // mismatch
+                line1[j] = "ACGTN"[ref[pt++]];
+                line2[j] = "ACGTN"[query[pq++]];
+                line3[j] = " x"[n==3];
+                break;
+            case 2: // insertion to ref
+                line1[j] = "ACGTN"[ref[pt++]];
+                line2[j] = '-';
+                line3[j] = '-';
+                break;
+            case 1: // insertion to query
+                line1[j] = '-';
+                line2[j] = "ACGTN"[query[pq++]];
+                line3[j] = '+';
+                break;
+            }
+
+            if (++j == sizeof(line1)) {
+                fprintf(bcftools_stderr, "%.*s\n", j, line1);
+                fprintf(bcftools_stderr, "%.*s\n", j, line2);
+                fprintf(bcftools_stderr, "%.*s\n", j, line3);
+                j = 0;
+            }
+        }
+        if (j) {
+            fprintf(bcftools_stderr, "%.*s\n", j, line1);
+            fprintf(bcftools_stderr, "%.*s\n", j, line2);
+            fprintf(bcftools_stderr, "%.*s\n", j, line3);
+        }
+    }
+#endif
+
+    // Aligned target length minus query length is an indication of the number
+    // of insertions and/or deletions.
+    // 
+    // For CIGAR 10M1I10M t_len > l_query ("AC"  / "ATC")
+    // For CIGAR 10M1D10M t_len < l_query ("ATC" / "AC")
+    // Hence t_len-l_query is -ve for net insertions and +ve for net deletions.
+    // If we compute nins and ndel directly via walking though EDLIB_TASK_PATH
+    // we'll see t_len-l_query == ndel-nins.
+    // 
+    // If a technology has a significantly higher chance of making deletion
+    // errors than insertion errors, then we would view deletions as less
+    // indicative of this sequence not coming from this candidate allele than
+    // if it had insertion (as the deletions are more likely to be errors
+    // rather than real, relative to the insertions).  Hence we can skew the
+    // score by the net delta of num_del - num_ins.
+    //
+    // Note this is an approximation that doesn't account for multiple
+    // insertions and deletions within the same sequence, but it is much faster
+    // as it doesn't require EDLIB_TASK_PATH to be computed.
+    //
+    // Given editDistance is +1 for every mismatch, insertion and deletion,
+    // provided the t_len-l_query multiplier < 1 then this is always +ve.
+
+    int t_len = *r.endLocations - *r.startLocations + 1;
+    int score = m*(r.editDistance - del_bias*(t_len - l_query));
+
+    edlibFreeAlignResult(r);
+    return score;
+}
+
+// Part of bcf_call_gap_prep.
+//
+// Realign using BAQ to get an alignment score of a single read vs
+// a haplotype consensus.  TODO: replace BAQ with something more robust.
+//
+// There are many coordinates, so let's explain them.
+// - left, right, tbeg, tend, r_start and r_end are in aligned reference
+//   coordinates.
+//   left/right start from pos +/- indel_win_size.
+//   r_start/r_end are the BAM first and last mapped coord on the reference.
+//   tbeg and tend are the intersection of the two.
+// - qbeg and qend are in BAM sequence coordinates
+// - qpos is in sequence coordinates, relative to qbeg.
+//
+// To see what this means, we have illustrations with coordinates
+// above the seqs in reference space and below the seqs in BAM seq space.
+//
+// Overlap left:
+//                     tbeg                        tend
+//      r_start        left                 pos    r_end          right
+// REF  :..............|--------------------#------:--------------|...
+// SEQ  :..............|--------------------#------|
+//      0              qbeg                 qpos   qend
+//
+// Overlap right:
+//                        r_start                     tend
+//         left           tbeg  pos                   right       r_end
+// REF  ...|--------------:-----#---------------------|...........:
+// SEQ                    |-----#---------------------|...........:
+//                        qbeg  qpos                  qend
+//                        0
+//
+// The "-" sequence is the bit passed in.
+// Ie ref2 spans left..right and query spans qbeg..qend.
+// We need to adjust ref2 therefore to tbeg..tend.
+//
+// Fills out score
+// Returns 0 on success,
+//        <0 on error
+static int bcf_cgp_align_score(bam_pileup1_t *p, bcf_callaux_t *bca,
+                               int type, int band,
+                               uint8_t *ref1, uint8_t *ref2, uint8_t *query,
+                               int r_start, int r_end,
+                               int tbeg, int tend1, int tend2,
+                               int left, int right,
+                               int qbeg, int qend,
+                               int pos, int qpos, int max_deletion,
+                               double qavg, double del_bias, int *score,
+                               int *str_len1_p, int *str_len2_p) {
+    int atype = abs(type);
+    int l, sc1, sc2;
+
+    // Trim poly_Ns at ends of ref.
+    // This helps to keep len(ref) and len(query) similar, to reduce
+    // band size and reduce the chance of -ve BAQ scores.
+    for (l = 0; l < tend1-tbeg && l < tend2-tbeg; l++)
+        if (ref1[l + tbeg-left] != 4 || ref2[l + tbeg-left] != 4)
+            break;
+    if (l > atype)
+        tbeg += l-atype;
+
+    for (l = tend1-tbeg-1; l >= 0; l--)
+        if (ref1[l + tbeg-left] != 4)
+            break;
+    l = tend1-tbeg-1 - l;
+    if (l > atype)
+        tend1 -= l-atype;
+
+    for (l = tend2-tbeg-1; l >= 0; l--)
+        if (ref2[l + tbeg-left] != 4)
+            break;
+    l = tend2-tbeg-1 - l;
+    if (l > atype) {
+        tend2 -= l-atype;
+    }
+
+    // The bottom 8 bits are length-normalised score while
+    // the top bits are unnormalised.
+    //
+    // Try original cons and new cons and pick best.
+    // This doesn't reduce FN much (infact maybe adds very slightly),
+    // but it does reduce GT errors and is a slight reduction to FP.
+
+    double mm = 30; // a const average qual for now. Could tune
+    sc2 = edlib_glocal(ref2 + tbeg - left, tend2 - tbeg,
+                       query, qend - qbeg, mm, del_bias);
+
+    if (tend1 != tend2 ||
+        memcmp((char *)ref1 + tbeg - left, (char *)ref2 + tbeg - left,
+               tend1 - tbeg) != 0)
+        sc1 = edlib_glocal(ref1 + tbeg - left, tend1 - tbeg,
+                           query, qend - qbeg, mm, del_bias);
+    else
+        sc1 = INT_MAX; // skip
+
+    // Find the best of the two alignments
+    if (sc1 < 0 && sc2 < 0) {
+        *score = 0xffffff;
+        return 0;
+    }
+    if (sc1 < 0) {
+        // sc2 is already correct
+    } else if (sc2 < 0) {
+        sc2 = sc1;
+    } else {
+        // sc1 and sc2 both pass, so use best
+        if (sc2 > sc1)
+            sc2 = sc1;
+    }
+
+    // Sc is overall alignment score, in top 24 bits (SeqQ). It's based
+    // purely on the scores for the whole alignment.
+    // We also have a separate indel score in bottom 8 bits (IndelQ).
+    // This is a function of all sorts of attributes of the candidate indel
+    // itself, such as STR length and the presence of poor quality bases.
+
+    // Used for adjusting indelQ below.  Lower l is more likely to call
+    // (--FN, ++FP).  (NB CLI --indel_bias is 1/indel_bias var).
+    // Starts as average score per base, and then adjusted based on seq
+    // complexity / quality.
+
+    l = .5*(100. * sc2 / (qend - qbeg) + .499);
+
+    *score = (sc2<<8) | (int)MIN(255, l * bca->indel_bias * .5);
+
+    return 0;
+}
+
+// Part of bcf_call_gap_prep.
+//
+// Returns n_alt on success
+//         -1 on failure
+
+// TODO: almost identical to bam2bcf_indel.c's copy, so we could share
+// the code and add a check on bca->edlib.
+static int bcf_cgp_compute_indelQ(int n, int *n_plp, bam_pileup1_t **plp,
+                                  bcf_callaux_t *bca, char *inscns,
+                                  int l_run, int max_ins,
+                                  int ref_type, int *types, int n_types,
+                                  double qavg, int *score,
+                                  int str_len1, int str_len2) {
+    // FIXME: n_types has a maximum; no need to alloc - use a #define?
+    int sc[MAX_TYPES], sumq[MAX_TYPES], s, i, j, t, K, n_alt, tmp;
+    memset(sumq, 0, n_types * sizeof(int));
+    int sum_indelQ1[100] = {0}; // n
+    int sum_indelQ2[100] = {0}; // n
+
+    // Confusing variable naming and bit usage.
+    //
+    // score[] is low 8  bits normalised (by len) alignment score
+    //            top 24 bits full alignment score
+    // This gets cast into "sct"; mnemonic score-per-indel-type.
+    //
+    // sc = (score<<6) | type  (index to types[] array for indel size)
+    // So sc>>14 = score>>(14-6) = score>>8.  Ie full alignment score
+    for (s = K = 0; s < n; ++s) {
+        for (i = 0; i < n_plp[s]; ++i, ++K) {
+            bam_pileup1_t *p = plp[s] + i;
+            // Labelling is confusing here.
+            //    sct is short for score.
+            //    sc is score + t(type)
+            // Why aren't these variable names reversed?
+            int *sct = &score[K*n_types], seqQ, indelQ1=0, indelQ2=0, indelQ=0;
+            for (t = 0; t < n_types; ++t) sc[t] = sct[t]<<6 | t;
+            for (t = 1; t < n_types; ++t) // insertion sort
+                for (j = t; j > 0 && sc[j] < sc[j-1]; --j)
+                    tmp = sc[j], sc[j] = sc[j-1], sc[j-1] = tmp;
+
+#ifdef ALIGN_DEBUG
+            fprintf(bcftools_stderr, "READ %s\tscores ", bam_get_qname(p->b));
+            for (t = 0; t < n_types; ++t) {
+                fprintf(bcftools_stderr, "%+2d/%-3d ", types[sc[t]&0x3f], sc[t]>>14);
+            }
+#endif
+
+            /* errmod_cal() assumes that if the call is wrong, the
+             * likelihoods of other events are equal. This is about
+             * right for substitutions, but is not desired for
+             * indels. To reuse errmod_cal(), I have to make
+             * compromise for multi-allelic indels.
+             */
+            if ((sc[0]&0x3f) == ref_type) {
+                // sc >> 14 is the total score.  It's been shifted by 8
+                // from normalised score and 6 from type.
+                // &0x3f is type number
+
+                // Best call is REF.  Compare vs best indel
+                indelQ = (sc[1]>>14) - (sc[0]>>14);
+                seqQ = est_seqQ(bca, types[sc[1]&0x3f], l_run, str_len1);
+            } else {
+                // look for the reference type
+                for (t = 0; t < n_types; ++t) {
+                    if ((sc[t]&0x3f) == ref_type)
+                        break;
+                }
+                indelQ = indelQ1 = (sc[t]>>14) - (sc[0]>>14);
+//                fprintf(bcftools_stderr, "IndelQ = %d: %d-%d",
+//                        indelQ, (sc[t]>>14), (sc[0]>>14));
+
+                // Best call is non-ref, compare vs next best non-ref,
+                // or ref if it's just 2 choices (most common case).
+                for (t = 1; t < n_types; t++)
+                    if ((sc[t]&0x3f) == ref_type)
+                        continue;
+                    else break;
+                if (t == n_types)
+                    t--; // it's ref, but it'll do as next best.
+                indelQ2 = (sc[t]>>14) - (sc[0]>>14);
+                seqQ = est_seqQ(bca, types[sc[0]&0x3f], l_run, str_len1);
+
+#if 1 // TEST 3
+                indelQ = bca->vs_ref*indelQ1 + (1-bca->vs_ref)*indelQ2;
+#endif
+            }
+
+            // So we lower qual in some, but raise the average to keep FN/FP
+            // ratios up.
+            // Is this key diff for PacBio old vs new HiFi?
+            indelQ  /= bca->indel_bias*0.5;
+            indelQ1 /= bca->indel_bias*0.5;
+
+            // Or maybe just *2 if bca->poly_mqual and be done with it?
+            // Or perhaps adjust the MIN(qavg/20, ...) to MIN(qavg/10) ?
+
+            // Skew SeqQ and IndelQ based on a portion of the minimum quality
+            // found within a homopolymer.  This is useful where the quality
+            // values are a bit mutable and move around in such data, but less
+            // so on clocked sequencing technologies.
+            //
+            // Enabling this causes lots of GT errors on Illumina.
+            // However on PacBio it's key to removal of false positives.
+            // ONT and UG seem somewhere inbetween.
+            if (bca->poly_mqual) { // TEST 4
+                int qpos = p->qpos, l;
+                uint8_t *seq = bam_get_seq(p->b);
+                uint8_t *qual = bam_get_qual(p->b);
+                int min_q = qual[qpos];
+
+                // scan homopolymer left
+                char baseL = bam_seqi(seq, qpos+1 < p->b->core.l_qseq
+                                      ? qpos+1 : qpos);
+                for (l = qpos; l >= 0; l--) {
+                    if (bam_seqi(seq, l) != baseL)
+                        break;
+                    if (min_q > qual[l])
+                        min_q = qual[l];
+                }
+
+                // scan homo-polymer right (including site of indel)
+                char base = bam_seqi(seq, qpos+1);
+                for (l = qpos+1; l < p->b->core.l_qseq; l++) {
+                    if (min_q > qual[l])
+                        min_q = qual[l];
+                    if (bam_seqi(seq, l) != base)
+                        break;
+                }
+
+                // We reduce -h so homopolymers get reduced likelihood of being
+                // called, but then optionally increase or decrease from there
+                // based on base quality.  Hence lack of low quality bases in
+                // homopolymer will rescue the score back again, reducing FNs.
+
+                // The score factors here may also be machine specific, but for
+                // now these work well (tuned on PB HiFi).
+                seqQ   += MIN(qavg/20,  min_q - qavg/10);
+                indelQ += MIN(qavg/20,  min_q - qavg/5);
+                indelQ1+= MIN(qavg/20,  min_q - qavg/5);
+
+                if (seqQ   < 0) seqQ   = 0;
+                if (indelQ < 0) indelQ = 0;
+                if (indelQ1< 0) indelQ1= 0;
+            }
+
+            // This is the length-normalised score from bcf_cgp_align_score
+            tmp = sc[0]>>6 & 0xff;
+
+            // reduce indelQ
+            // high score = bad, low score = good; flip for indelQ
+            // low normalised scores leave indelQ unmodified
+            // high normalised scores set indelQ to 0
+            // inbetween scores have a linear scale from indelQ to 0
+// Altering the MAGIC value below (originally 111, but chosen for unknown
+// reasons) is comparable to altering --indel-bias.
+#define TMP_MAGIC 255.0
+
+            indelQ = tmp > TMP_MAGIC? 0 : (int)((1. - tmp/TMP_MAGIC) * indelQ + .499);
+            indelQ1= tmp > TMP_MAGIC? 0 : (int)((1. - tmp/TMP_MAGIC) * indelQ1+ .499);
+
+            indelQ  = MIN(indelQ,  255);
+            indelQ1 = MIN(indelQ1, 255);
+
+            // Doesn't really help accuracy, but permits -h to take
+            // affect still.
+            if (indelQ > seqQ) indelQ = seqQ;
+            if (indelQ > 255) indelQ = 255;
+            if (indelQ1> 255) indelQ1= 255;
+            if (seqQ > 255) seqQ = 255;
+
+            // Use 22 bits in total.
+            // 0-7   IndelQ
+            // 8-15  SeqQ
+            // 16-22 Score-per-base
+            p->aux = (sc[0]&0x3f)<<16 | seqQ<<8 | indelQ;
+            sumq[sc[0]&0x3f] += indelQ;
+
+#ifdef ALIGN_DEBUG
+            fprintf(bcftools_stderr, "\t%d\t%d\n", indelQ, seqQ);
+#endif
+
+            // Experiment in p->aux vs sumq.
+            // One gives likelihood of an indel being here, while the other
+            // is likelihood of a specific genotype?  But which is which?
+
+            sum_indelQ1[s] += indelQ1;
+            sum_indelQ2[s] += indelQ;
+        }
+    }
+
+    // Determine bca->indel_types[] and bca->inscns.
+    // Sumq[0] is always reference.
+    // Sumq[1] is best non-ref (and maybe better than ref)
+    bca->maxins = max_ins;
+    bca->inscns = (char*) realloc(bca->inscns, bca->maxins * 4);
+    if (bca->maxins && !bca->inscns)
+        return -1;
+    for (t = 0; t < n_types; ++t)
+        sumq[t] = sumq[t]<<6 | t;
+    for (t = 1; t < n_types; ++t) // insertion sort
+        for (j = t; j > 0 && sumq[j] > sumq[j-1]; --j)
+            tmp = sumq[j], sumq[j] = sumq[j-1], sumq[j-1] = tmp;
+    for (t = 0; t < n_types; ++t) // look for the reference type
+        if ((sumq[t]&0x3f) == ref_type) break;
+
+    if (t) { // then move the reference type to the first
+        tmp = sumq[t];
+        for (; t > 0; --t) sumq[t] = sumq[t-1];
+        sumq[0] = tmp;
+    }
+
+    for (t = 0; t < 4; ++t) bca->indel_types[t] = B2B_INDEL_NULL;
+    for (t = 0; t < 4 && t < n_types; ++t) {
+        bca->indel_types[t] = types[sumq[t]&0x3f];
+#ifdef ALIGN_DEBUG
+        fprintf(bcftools_stderr, "TYPE %+2d %d\n", types[t], sumq[t]>>6);
+#endif
+        if (bca->maxins) // potentially an insertion
+            memcpy(&bca->inscns[t * bca->maxins],
+                   &inscns[(sumq[t]&0x3f) * max_ins], bca->maxins);
+    }
+
+    // Update p->aux.
+    // If per-alignment type isn't found, then indelQ/seqQ is 0,
+    // otherwise unchanged.
+    for (s = n_alt = 0; s < n; ++s) {
+        for (i = 0; i < n_plp[s]; ++i) {
+            bam_pileup1_t *p = plp[s] + i;
+            int x = types[p->aux>>16&0x3f];
+            for (j = 0; j < 4; ++j)
+                if (x == bca->indel_types[j]) break;
+            p->aux = j<<16 | (j == 4? 0 : (p->aux&0xffff));
+            if ((p->aux>>16&0x3f) > 0) ++n_alt;
+#ifdef ALIGN_DEBUG
+            fprintf(bcftools_stderr, "FIN %s\t%d\t%d\t%d\n",
+                    bam_get_qname(p->b), (p->aux>>16)&0x3f,
+                    bca->indel_types[(p->aux>>16)&0x3f], p->aux&0xff);
+#endif
+        }
+    }
+
+    return n_alt;
+}
+
+/*
+FIXME: with high number of samples, do we handle IMF correctly?  Is it
+fraction of indels across entire data set, or just fraction for this
+specific sample? Needs to check bca->per_sample_flt (--per-sample-mF) opt.
+ */
+
+/*
+    notes:
+    - n .. number of samples
+    - the routine sets bam_pileup1_t.aux of each read as follows:
+        - 6: unused
+        - 6: the call; index to bcf_callaux_t.indel_types   .. (aux>>16)&0x3f
+        - 8: estimated sequence quality                     .. (aux>>8)&0xff
+        - 8: indel quality                                  .. aux&0xff
+ */
+int bcf_edlib_gap_prep(int n, int *n_plp, bam_pileup1_t **plp, int pos,
+                      bcf_callaux_t *bca, const char *ref, int ref_len)
+{
+    if (ref == 0 || bca == 0) return -1;
+
+    int i, s, t, n_types, *types = NULL, max_rd_len, left, right, max_ins;
+    int *score = NULL;
+    int N, K, l_run, ref_type, n_alt = -1;
+    char *inscns = NULL, *query = NULL;
+
+    // determine if there is a gap
+    for (s = N = 0; s < n; ++s) {
+        for (i = 0; i < n_plp[s]; ++i)
+            if (plp[s][i].indel != 0) break;
+        if (i < n_plp[s]) break;
+    }
+    if (s == n)
+        // there is no indel at this position.
+        return -1;
+
+    // Find average base quality over this region
+    double qavg = 30, qsum = 0, qcount = 0;
+    int qmax = 0;
+    for (s = 0; s < n; s++) {
+        for (i = 0; i < n_plp[s]; i++) {
+#define QWIN 50
+            bam_pileup1_t *p = plp[s] + i;
+            int kstart = p->qpos - QWIN > 0 ? p->qpos - QWIN : 0;
+            int kend = p->qpos + QWIN < p->b->core.l_qseq
+                ? p->qpos + QWIN : p->b->core.l_qseq;
+            uint8_t *qual = bam_get_qual(p->b);
+            int k;
+            for (k = kstart; k < kend; k++) {
+                qsum += qual[k];
+                qcount++;
+                if (qmax < qual[k])
+                    qmax = qual[k];
+            }
+        }
+    }
+    qavg = (qsum+1) / (qcount+1);
+
+    // find out how many types of indels are present
+    types = bcf_cgp_find_types(n, n_plp, plp, pos, bca, ref,
+                               &max_rd_len, &n_types, &ref_type, &N);
+    if (!types)
+        goto err;
+
+
+    // calculate left and right boundary, based on type size for a bit more
+    // speed.
+    int max_indel = 20*MAX(ABS(types[0]), ABS(types[n_types-1]))
+                  + bca->indel_win_size/4;
+    if (max_indel > bca->indel_win_size)
+        max_indel = bca->indel_win_size;
+    left = pos > max_indel ? pos - max_indel : 0;
+    right = pos + max_indel;
+
+    int del_size = types[0]<0 ? -types[0] : 0;
+    right += del_size;
+
+    // in case the alignments stand out the reference
+    for (i = pos; i < right; ++i)
+        if (ref[i] == 0) break;
+    right = i;
+
+    // compute the likelihood given each type of indel for each read
+    max_ins = types[n_types - 1];   // max_ins is at least 0
+
+    // The length of the homopolymer run around the current position
+    l_run = bcf_cgp_l_run(ref, pos);
+    int l_run_base = seq_nt16_table[(uint8_t)ref[pos+1]];
+    int l_run_ins = 0;
+
+    // construct the consensus sequence (minus indels, which are added later)
+    if (max_ins > 0) {
+        // TODO: replace filling inscns[] with calc_consensus return
+        // so the merges of the insertion consensus for type[t] is
+        // reported directly.  (It may need adjustment to avoid N)
+        inscns = bcf_cgp_calc_ins_cons(n, n_plp, plp, pos,
+                                       types, n_types, max_ins, s);
+        if (!inscns)
+            return -1;
+    }
+
+    query = (char*) calloc(right - left + max_rd_len + max_ins + 2, 1);
+    score = (int*) calloc(N * n_types, sizeof(int));
+    bca->indelreg = 0;
+    double nqual_over_60 = bca->nqual / 60.0;
+
+    int biggest_del = 0;
+    int biggest_ins = 0;
+    for (t = 0; t < n_types; t++) {
+        if (biggest_del > types[t])
+            biggest_del = types[t];
+        if (biggest_ins < types[t])
+            biggest_ins = types[t];
+    }
+    int band = biggest_ins - biggest_del; // NB del is -ve
+
+    // Find left & right extents of STR covering pos, from ref
+    int pos_l = pos, pos_r = pos;
+    {
+        rep_ele *reps, *elt, *tmp;
+        int pstart = MAX(0, pos-30);
+        int pmid = pos-pstart;
+        int pend = MIN(ref_len, pos+30);
+        reps = find_STR((char *)&ref[pstart], pend-pstart, 0);
+        DL_FOREACH_SAFE(reps, elt, tmp) {
+            if (elt->end >= pmid && elt->start <= pmid) {
+                if (pos_l > pstart + elt->start)
+                    pos_l = pstart + elt->start;
+                if (pos_r < pstart + elt->end)
+                    pos_r = pstart + elt->end;
+            }
+            DL_DELETE(reps, elt);
+            free(elt);
+        }
+    }
+
+    int str_len1 = l_run, str_len2 = l_run/4;
+    for (t = 0; t < n_types; ++t) {
+        int l, ir;
+
+        // Compute indelreg.  This is the context in the reference.  Eg:
+        //
+        // REF:  AG--TTTC  Inscns   is "TT".
+        // SEQ:  AGTTTTTC  Indelreg is 3; next 3 "TTT" bases
+        //
+        // => GTTT GTTTTT is call.
+        if (types[t] == 0)
+            ir = 0;
+        else if (types[t] > 0)
+            ir = est_indelreg(pos, ref, types[t], &inscns[t*max_ins]);
+        else
+            ir = est_indelreg(pos, ref, -types[t], 0);
+
+        if (ir > bca->indelreg)
+            bca->indelreg = ir;
+
+        // Realignment score, computed via BAQ
+        for (s = K = 0; s < n; ++s) {
+            char **tcons;
+            int left_shift, right_shift;
+            int tcon_len[2];
+            int cpos_pos;
+            tcons = bcf_cgp_consensus(n, n_plp, plp, pos, bca, ref, ref_len,
+                                      left, right, s, types[t], biggest_del,
+                                      &left_shift, &right_shift, &band,
+                                      tcon_len, &cpos_pos, pos_l, pos_r);
+            // TODO: Consensus for a deletion shouldn't match the
+            // consensus for type 0.  Eg consider
+            //         vv                          vv
+            // REF:  AATGTGTGAACAA        REF:   AATGTG--AACAA
+            // T0:   AATGTG--AACAA        T0:    AATGTG--AACAA
+            // T-2:  AA--TGTGAATAA        T-2:   AA--TGTGAATAA:
+            //
+            // On left: both T0 and T-2 are the same length, as it's
+            // just a deletion that moved.  We may end up assigning
+            // reads to an indel allele based on the SNP they have and
+            // not the actual indel.
+            // There *is* a deletion here though, but only 1.  How do
+            // we call it once only?  Need to replace entire region
+            // with a reassembly.
+            //
+            // On right: T0 and T-2 have same length again, but there
+            // isn't an indel as it's ins+del vs del+ins. They're
+            // also the same length as the REF for this region.
+            // Hence likelihood of this variant existing is tied in
+            // with their equal and high similarity with/to the ref.
+            //
+            // We could do an alignment of tcons[0] and tcons[1] and check
+            // whether their differences are consistent with (ie the
+            // hamming distance is at least ABS(types[t]/2).  I don't think
+            // it'll rescue many FPs though.
+
+#ifdef CONS_DEBUG
+            {
+                int j;
+                for (j = 0; j < 2; j++) {
+                    int k;
+                    fprintf(bcftools_stderr, "Cons%d @ %d %4d/%4d ",
+                            j, pos, types[t], left_shift);
+                    for (k = 0; k < tcon_len[j]; k++) {
+                        if (k == cpos_pos)
+                            putc('#', bcftools_stderr);
+                        putc("ACGTN"[(uint8_t)tcons[j][k]], bcftools_stderr);
+                    }
+                    putc('\n', bcftools_stderr);
+                }
+            }
+#endif
+
+            // Scan for base-runs in the insertion.
+            // We use this to avoid over-correction in est_seqQ when the
+            // insertion is not part of the neighbouring homopolymer.
+            int k = tcons[0][cpos_pos], j;
+            for (j = 0; j < types[t]; j++)
+                if (tcons[0][cpos_pos+j] != k)
+                    break;
+            if (j && j == types[t])
+                l_run_ins |= "\x1\x2\x4\x8\xf"[k]; // ACGTN
+            if (types[t] < 0)
+                l_run_ins |= 0xff;
+
+            // align each read to consensus(es)
+            for (i = 0; i < n_plp[s]; ++i, ++K) {
+                bam_pileup1_t *p = plp[s] + i;
+
+                // Some basic ref vs alt stats.
+                int imq = p->b->core.qual > 59 ? 59 : p->b->core.qual;
+                imq *= nqual_over_60;
+
+                int sc_len, slen, epos, sc_end;
+
+                // Only need to gather stats on one type, as it's
+                // identical calculation for all the subsequent ones
+                // and we're sharing the same stats array
+                if (t == 0) {
+                    // Gather stats for INFO field to aid filtering.
+                    // mq and sc_len not very helpful for filtering, but could
+                    // help in assigning a better QUAL value.
+                    //
+                    // Pos is slightly useful.
+                    // Base qual can be useful, but need qual prior to BAQ?
+                    // May need to cache orig quals in aux tag so we can fetch
+                    // them even after mpileup step.
+                    get_pos(bca, p, &sc_len, &slen, &epos, &sc_end);
+
+                    assert(imq >= 0 && imq < bca->nqual);
+                    assert(epos >= 0 && epos < bca->npos);
+                    assert(sc_len >= 0 && sc_len < 100);
+                    if (p->indel) {
+                        bca->ialt_mq[imq]++;
+                        bca->ialt_scl[sc_len]++;
+                        bca->ialt_pos[epos]++;
+                    } else {
+                        bca->iref_mq[imq]++;
+                        bca->iref_scl[sc_len]++;
+                        bca->iref_pos[epos]++;
+                    }
+                }
+
+                int qbeg, qpos, qend, tbeg, tend, kk;
+                uint8_t *seq = bam_get_seq(p->b);
+                uint32_t *cigar = bam_get_cigar(p->b);
+                if (p->b->core.flag & BAM_FUNMAP) continue;
+
+                // FIXME: the following loop should be better moved outside;
+                // nonetheless, realignment should be much slower anyway.
+                for (kk = 0; kk < p->b->core.n_cigar; ++kk)
+                    if ((cigar[kk]&BAM_CIGAR_MASK) == BAM_CREF_SKIP)
+                        break;
+                if (kk < p->b->core.n_cigar)
+                    continue;
+
+                // determine the start and end of sequences for alignment
+                int left2 = left, right2 = right;
+                int min_win_size = MAX(-biggest_del, biggest_ins);
+                min_win_size += ABS(left_shift) + ABS(right_shift);
+                {
+                    rep_ele *reps, *elt, *tmp;
+                    reps = find_STR(tcons[0], tcon_len[0], 0);
+                    //int max_str = 0;
+                    int tot_str = 0;
+                    DL_FOREACH_SAFE(reps, elt, tmp) {
+                        // if (max_str < elt->end - elt->start)
+                        //     max_str = elt->end - elt->start;
+                        tot_str += elt->end - elt->start;
+                        DL_DELETE(reps, elt);
+                        free(elt);
+                    }
+
+                    // Ideally max_str should be enough, but it's still not
+                    // sufficient in longer range some repeats.
+                    //min_win_size += max_str;
+                    min_win_size += tot_str;
+                }
+                min_win_size += 10;
+
+// TEST 8
+                if (p->b->core.l_qseq > 1000) {
+                    // long read data needs less context.  It also tends to
+                    // have many more candidate indels to investigate so
+                    // speed here matters more.
+                    if (pos - left >= min_win_size)
+                        left2 = MAX(left2, pos - min_win_size);
+                    if (right-pos >= min_win_size)
+                        right2 = MIN(right2, pos + min_win_size);
+                }
+
+                // Genomic coords for first and last base of query
+                // alignment.  This is only used in bcf_cgp_align_score
+                // for computing scores by looking for the proximity
+                // of STRs with the end of the query alignment.
+                int r_start = p->b->core.pos;
+                int r_end = bam_cigar2rlen(p->b->core.n_cigar,
+                                           bam_get_cigar(p->b));
+                r_end += -1 + r_start;
+
+
+                // Map left2/right2 genomic coordinates to qbeg/qend
+                // query coordinates.  The query may not span the
+                // entire left/right region, so this also returns the
+                // equivalent genomic coords for qbeg/qend in tbeg/tend.
+                qbeg = tpos2qpos(&p->b->core, bam_get_cigar(p->b),
+                                 left2, 0, &tbeg);
+                qpos = tpos2qpos(&p->b->core, bam_get_cigar(p->b), pos,
+                                     0, &tend) - qbeg;
+                qend = tpos2qpos(&p->b->core, bam_get_cigar(p->b),
+                                 right2, 1, &tend);
+
+                int old_tend = tend;
+                int old_tbeg = tbeg;
+
+                // write the query sequence
+                for (l = qbeg; l < qend; ++l)
+                    query[l - qbeg] = seq_nt16_int[bam_seqi(seq, l)];
+
+                // tbeg and tend are the genomic locations equivalent
+                // to qbeg and qend on the sequence.
+                // These may being entirely within our left/right
+                // coordinates over which we've computed the
+                // consensus, or overlapping to left/right.
+                //
+                // We know an estimation of band, plus biggest indel,
+                // so we can trim tbeg/tend to a smaller region if we
+                // wish here.  This speeds up BAQ scoring.
+                int wband = band + MAX(-biggest_del, biggest_ins)*2 + 20;
+                int tend1 = left + tcon_len[0] - (left2-left);
+                int tend2 = left + tcon_len[1] - (left2-left);
+                tend1 = MIN(tend1, old_tend + wband);
+                tend2 = MIN(tend2, old_tend + wband);
+                tbeg = MAX(left2, old_tbeg - wband);
+
+                // do realignment; this is the bottleneck.
+                //
+                // Note low score = good, high score = bad.
+                if (tend1 > tbeg && tend2 > tbeg) {
+                    //fprintf(bcftools_stderr, "Num %d\n", i);
+                    if (bcf_cgp_align_score(p, bca, types[t], band,
+                                            (uint8_t *)tcons[0] + left2-left,
+                                            (uint8_t *)tcons[1] + left2-left,
+                                            (uint8_t *)query,
+                                            r_start, r_end,
+                                            tbeg, tend1, tend2,
+                                            left2, left + tcon_len[0],
+                                            qbeg, qend, pos,qpos, -biggest_del,
+                                            qavg, bca->del_bias,
+                                            &score[K*n_types + t],
+                                            &str_len1, &str_len2) < 0) {
+                        goto err;
+                    }
+#ifdef ALIGN_DEBUG
+                    fprintf(bcftools_stderr, "type %d %x / %x\t%s\n",
+                            types[t],
+                            score[K*n_types + t] >> 8,
+                            score[K*n_types + t] & 0xff,
+                            bam_get_qname(p->b));
+#endif
+                } else {
+                    // place holder large cost for reads that cover the
+                    // region entirely within a deletion (thus tend < tbeg).
+                    score[K*n_types + t] = 0xffffff;
+                }
+            }
+            free(tcons);
+        }
+    }
+
+    // compute indelQ
+    if (!(l_run_base & l_run_ins))
+        l_run = 1; // different base type in ins to flanking region.
+    n_alt = bcf_cgp_compute_indelQ(n, n_plp, plp, bca, inscns, l_run, max_ins,
+                                   ref_type, types, n_types, qavg, score,
+                                   str_len1, str_len2);
+
+ err:
+    // free
+    free(query);
+    free(score);
+    free(types);
+    free(inscns);
+
+    return n_alt > 0? 0 : -1;
+}
index 2e0add15ac2e3d16e02f63277a1798a3a86b6071..3fe4fdea74d1687910a803da4e31121c57eeb97d 100644 (file)
@@ -396,7 +396,7 @@ static int iaux_set_consensus(indel_aux_t *iaux, int ismpl)
 // Finds the smallest index in the seq_pos array holding value equal to pos, or if there is no
 // such value, the largest index with value smaller than pos. Starts at initial guess ioff.
 // This could use a binary search but the assumption is that the initial guess is indel-size close
-// to the actuall coordinate.
+// to the actual coordinate.
 //
 // TODO: remove this function and seq_pos from cns creation as it seems unnecessary
 static int find_ref_offset(hts_pos_t pos, hts_pos_t *seq_pos, int nseq_pos, int ioff)
index c8bea99cb135f5c2ad6dbf364ad42467e7943617..5309dca3bc1270ecd50fd4dff53fca79db6a93f7 100644 (file)
@@ -398,7 +398,7 @@ static int iaux_set_consensus(indel_aux_t *iaux, int ismpl)
 // Finds the smallest index in the seq_pos array holding value equal to pos, or if there is no
 // such value, the largest index with value smaller than pos. Starts at initial guess ioff.
 // This could use a binary search but the assumption is that the initial guess is indel-size close
-// to the actuall coordinate.
+// to the actual coordinate.
 //
 // TODO: remove this function and seq_pos from cns creation as it seems unnecessary
 static int find_ref_offset(hts_pos_t pos, hts_pos_t *seq_pos, int nseq_pos, int ioff)
index faedc3fef3c4376fc9759f53e0c1d31ef71bed30..975504f8aea3d70f5cf2eff5e91f5d2503908cb6 100644 (file)
@@ -45,7 +45,7 @@ KSORT_INIT_GENERIC(uint32_t)
 //
 // *_tpos is returned as tpos if query overlaps tpos, but for deletions
 // it'll be either the start (is_left) or end (!is_left) ref position.
-static int tpos2qpos(const bam1_core_t *c, const uint32_t *cigar, int32_t tpos, int is_left, int32_t *_tpos)
+int tpos2qpos(const bam1_core_t *c, const uint32_t *cigar, int32_t tpos, int is_left, int32_t *_tpos)
 {
     // x = pos in ref, y = pos in query seq
     int k, x = c->pos, y = 0, last_y = 0;
@@ -98,8 +98,8 @@ inline int est_indelreg(int pos, const char *ref, int l, char *ins4)
 }
 
 // Identify spft-clip length, position in seq, and clipped seq len
-static inline void get_pos(const bcf_callaux_t *bca, bam_pileup1_t *p,
-                           int *sc_len_r, int *slen_r, int *epos_r, int *end) {
+void get_pos(const bcf_callaux_t *bca, bam_pileup1_t *p,
+             int *sc_len_r, int *slen_r, int *epos_r, int *end) {
     bam1_t *b = p->b;
     int sc_len = 0, sc_dist = -1, at_left = 1;
     int epos = p->qpos, slen = b->core.l_qseq;
@@ -155,6 +155,7 @@ static inline void get_pos(const bcf_callaux_t *bca, bam_pileup1_t *p,
 //
 // Scans the pileup to identify all the different sizes of indels
 // present.
+// types[] returned is sorted by size, from smallest (maybe negative) to largest.
 //
 // Returns types and fills out n_types_r,  max_rd_len_r and ref_type_r,
 //         or NULL on error.
@@ -429,9 +430,9 @@ int bcf_cgp_l_run(const char *ref, int pos) {
 
 // Compute the consensus for this sample 's', minus indels which
 // get added later.
-static char *bcf_cgp_calc_cons(int n, int *n_plp, bam_pileup1_t **plp,
-                               int pos, int *types, int n_types,
-                               int max_ins, int s) {
+char *bcf_cgp_calc_cons(int n, int *n_plp, bam_pileup1_t **plp,
+                        int pos, int *types, int n_types,
+                        int max_ins, int s) {
     int i, j, t, k;
     int *inscns_aux = (int*)calloc(5 * n_types * max_ins, sizeof(int));
     if (!inscns_aux)
index 65a7179dfd2e9bb3e27186e8550a132c2484b458..aded528b39aa044e8e5d2df402cf5f283edbad67 100644 (file)
@@ -47,7 +47,7 @@ KSORT_INIT_GENERIC(uint32_t)
 //
 // *_tpos is returned as tpos if query overlaps tpos, but for deletions
 // it'll be either the start (is_left) or end (!is_left) ref position.
-static int tpos2qpos(const bam1_core_t *c, const uint32_t *cigar, int32_t tpos, int is_left, int32_t *_tpos)
+int tpos2qpos(const bam1_core_t *c, const uint32_t *cigar, int32_t tpos, int is_left, int32_t *_tpos)
 {
     // x = pos in ref, y = pos in query seq
     int k, x = c->pos, y = 0, last_y = 0;
@@ -100,8 +100,8 @@ inline int est_indelreg(int pos, const char *ref, int l, char *ins4)
 }
 
 // Identify spft-clip length, position in seq, and clipped seq len
-static inline void get_pos(const bcf_callaux_t *bca, bam_pileup1_t *p,
-                           int *sc_len_r, int *slen_r, int *epos_r, int *end) {
+void get_pos(const bcf_callaux_t *bca, bam_pileup1_t *p,
+             int *sc_len_r, int *slen_r, int *epos_r, int *end) {
     bam1_t *b = p->b;
     int sc_len = 0, sc_dist = -1, at_left = 1;
     int epos = p->qpos, slen = b->core.l_qseq;
@@ -157,6 +157,7 @@ static inline void get_pos(const bcf_callaux_t *bca, bam_pileup1_t *p,
 //
 // Scans the pileup to identify all the different sizes of indels
 // present.
+// types[] returned is sorted by size, from smallest (maybe negative) to largest.
 //
 // Returns types and fills out n_types_r,  max_rd_len_r and ref_type_r,
 //         or NULL on error.
@@ -431,9 +432,9 @@ int bcf_cgp_l_run(const char *ref, int pos) {
 
 // Compute the consensus for this sample 's', minus indels which
 // get added later.
-static char *bcf_cgp_calc_cons(int n, int *n_plp, bam_pileup1_t **plp,
-                               int pos, int *types, int n_types,
-                               int max_ins, int s) {
+char *bcf_cgp_calc_cons(int n, int *n_plp, bam_pileup1_t **plp,
+                        int pos, int *types, int n_types,
+                        int max_ins, int s) {
     int i, j, t, k;
     int *inscns_aux = (int*)calloc(5 * n_types * max_ins, sizeof(int));
     if (!inscns_aux)
index bba71e3b63ff0bb81801b271a20a6aa7653bd04d..51c2d040fd530a523097bf960d98a0565a270470 100644 (file)
@@ -1,6 +1,6 @@
 /*  bcftools.h -- utility function declarations.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -29,6 +29,7 @@ THE SOFTWARE.  */
 #include <htslib/hts_defs.h>
 #include <htslib/vcf.h>
 #include <htslib/synced_bcf_reader.h>
+#include <htslib/kfunc.h>
 #include <math.h>
 
 #define FT_TAB_TEXT 0       // custom tab-delimited text file
@@ -50,7 +51,13 @@ void error(const char *format, ...) HTS_NORETURN HTS_FORMAT(HTS_PRINTF_FMT, 1, 2
 void error_errno(const char *format, ...) HTS_NORETURN HTS_FORMAT(HTS_PRINTF_FMT, 1, 2);
 
 // For on the fly index creation with --write-index
-int init_index(htsFile *fh, bcf_hdr_t *hdr, char *fname, char **idx_fname);
+int init_index2(htsFile *fh, bcf_hdr_t *hdr, const char *fname, char **idx_fname, int idx_fmt);
+int init_index(htsFile *fh, bcf_hdr_t *hdr, const char *fname, char **idx_fname);
+
+// Used to set args->write_index in CLI.
+// It will be true if set correctly.
+// Note due to HTS_FMT_CSI being zero we have to use an additional bit.
+int write_index_parse(char *arg);
 
 void bcf_hdr_append_version(bcf_hdr_t *hdr, int argc, char **argv, const char *cmd);
 const char *hts_bcf_wmode(int file_type);
@@ -60,6 +67,10 @@ char *init_tmp_prefix(const char *prefix);
 int read_AF(bcf_sr_regions_t *tgt, bcf1_t *line, double *alt_freq);
 int parse_overlap_option(const char *arg);
 
+// Default sort order: chr,pos,alleles
+int cmp_bcf_pos(const void *aptr, const void *bptr);
+int cmp_bcf_pos_ref_alt(const void *aptr, const void *bptr);
+
 static inline int iupac2bitmask(char iupac)
 {
     const int A = 1;
@@ -121,6 +132,23 @@ static inline double phred_score(double prob)
     return prob>99 ? 99 : prob;
 }
 
+static inline double calc_binom_two_sided(int na, int nb, double aprob)
+{
+    if ( !na && !nb ) return -1;
+    if ( na==nb ) return 1;
+
+    // kfunc.h implements kf_betai, which is the regularized beta function  P(X<=k/N;p) = I_{1-p}(N-k,k+1)
+
+    double prob = na > nb ? 2 * kf_betai(na, nb+1, aprob) : 2 * kf_betai(nb, na+1, aprob);
+
+    if ( prob > 1 ) prob = 1;   // this can happen, machine precision error, eg. kf_betai(1,0,0.5)
+    return prob;
+}
+static inline double calc_binom_one_sided(int na, int nb, double aprob, int ge)
+{
+    return ge ? kf_betai(na, nb + 1, aprob) : kf_betai(nb, na + 1, 1 - aprob);
+}
+
 static const uint64_t bcf_double_missing    = 0x7ff0000000000001;
 static const uint64_t bcf_double_vector_end = 0x7ff0000000000002;
 static inline void bcf_double_set(double *ptr, uint64_t value)
@@ -141,4 +169,16 @@ static inline int bcf_double_test(double d, uint64_t value)
 #define bcf_double_is_missing(x)     bcf_double_test((x),bcf_double_missing)
 #define bcf_double_is_missing_or_vector_end(x)     (bcf_double_test((x),bcf_double_missing) || bcf_double_test((x),bcf_double_vector_end))
 
+static inline int get_unseen_allele(bcf1_t *line)
+{
+    int i;
+    for (i=1; i<line->n_allele; i++)
+    {
+        if ( !strcmp(line->d.allele[i],"<*>") ) return i;
+        if ( !strcmp(line->d.allele[i],"<NON_REF>") ) return i;
+        if ( !strcmp(line->d.allele[i],"<X>") ) return i;
+    }
+    return 0;
+}
+
 #endif
index 16bf0b68e558064e75a94b37f1ca8e678f7adf3d..090ac019a20a57db62f7916ebe04d9a67bfb04a7 100644 (file)
@@ -33,7 +33,7 @@ THE SOFTWARE.  */
 #define CALL_VARONLY        (1<<1)
 #define CALL_CONSTR_TRIO    (1<<2)
 #define CALL_CONSTR_ALLELES (1<<3)
-//
+#define CALL_KEEP_UNSEEN    (1<<4)
 #define CALL_FMT_PV4        (1<<5)
 #define CALL_FMT_GQ         (1<<6)
 #define CALL_FMT_GP         (1<<7)
@@ -125,8 +125,7 @@ call_t;
 void error(const char *format, ...);
 
 /*
- *  call() - return -1 value on critical error; -2 to skip the site; or the number of non-reference
- *            alleles on success.
+ *  call() - return -1 value on critical error; -2 to skip the site; or the number of alleles on success
  */
 int mcall(call_t *call, bcf1_t *rec);    // multiallic and rare-variant calling model
 int ccall(call_t *call, bcf1_t *rec);    // the default consensus calling model
index 2b58670c74c5c06ead220c620d6a43e66b08d378..54f17c221d4859e4996e7cd56fbcc2d8d7b279b7 100644 (file)
@@ -1,6 +1,6 @@
 /* The MIT License
 
-   Copyright (c) 2014-2023 Genome Research Ltd.
+   Copyright (c) 2014-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -118,7 +118,7 @@ typedef struct
     char **argv;
     int argc, output_iupac, iupac_GTs, haplotype, allele, isample, napplied;
     uint8_t *iupac_bitmask, *iupac_als;
-    int miupac_bitmask, miupac_als;
+    int miupac_bitmask, miupac_als, regions_overlap;
     char *fname, *ref_fname, *sample, *sample_fname, *output_fname, *mask_fname, *chain_fname, missing_allele, absent_allele;
     char mark_del, mark_ins, mark_snv;
     smpl_ilist_t *smpl;
@@ -229,7 +229,14 @@ static void init_data(args_t *args)
     args->hdr = args->files->readers[0].header;
     args->isample = -1;
     if ( !args->sample )
+    {
         args->smpl = smpl_ilist_init(args->hdr,NULL,0,SMPL_NONE|SMPL_VERBOSE);
+        if ( !args->smpl->n )
+        {
+            smpl_ilist_destroy(args->smpl);
+            args->smpl = NULL;
+        }
+    }
     else if ( args->sample && strcmp("-",args->sample) )
     {
         args->smpl = smpl_ilist_init(args->hdr,args->sample,0,SMPL_NONE|SMPL_VERBOSE);
@@ -244,12 +251,22 @@ static void init_data(args_t *args)
     {
         if ( args->haplotype || args->allele )
         {
-            if ( args->smpl->n > 1 ) error("Too many samples, only one can be used with -H\n");
+            if ( args->smpl->n > 1 ) error("Too many samples, only one can be used with -H; check the -s,-S options\n");
             args->isample = args->smpl->idx[0];
         }
         else
+        {
             args->iupac_GTs = 1;
+            if ( args->smpl->n==1 )
+                fprintf(stderr,"Note: applying IUPAC codes based on FORMAT/GT in sample %s\n",bcf_hdr_int2id(args->hdr,BCF_DT_SAMPLE,args->smpl->idx[0]));
+            else
+                fprintf(stderr,"Note: applying IUPAC codes based on FORMAT/GT in %d samples\n",args->smpl->n);
+        }
     }
+    else if ( args->output_iupac )
+        fprintf(stderr,"Note: applying IUPAC codes based on REF,ALT%s\n",bcf_hdr_nsamples(args->hdr)?", ignoring samples":"");
+    else
+        fprintf(stderr,"Note: applying REF,ALT variants%s\n",bcf_hdr_nsamples(args->hdr)?", ignoring samples":"");
     int i;
     for (i=0; i<args->nmask; i++)
     {
@@ -272,7 +289,6 @@ static void init_data(args_t *args)
         if ( ! args->fp_out ) error("Failed to create %s: %s\n", args->output_fname, strerror(errno));
     }
     else args->fp_out = stdout;
-    if ( args->isample<0 && !args->iupac_GTs ) fprintf(stderr,"Note: the --samples option not given, applying all records regardless of the genotype\n");
     if ( args->filter_str )
         args->filter = filter_init(args->hdr, args->filter_str);
     args->rid = -1;
@@ -348,20 +364,28 @@ static void init_region(args_t *args, char *line)
     args->prev_base_pos = -1;
     args->fa_buf.l  = 0;
     args->fa_length = 0;
-    args->fa_end_pos = to;
-    args->fa_ori_pos = from;
-    args->fa_src_pos = from;
+    args->fa_end_pos = to;      // 0-based
+    args->fa_ori_pos = from;    // 0-based
+    args->fa_src_pos = from;    // 0-based
     args->fa_mod_off = 0;
     args->fa_frz_pos = -1;
     args->fa_frz_mod = -1;
     args->fa_case    = -1;
     args->vcf_rbuf.n = 0;
 
+
+    // bcf_sr_set_regions accepts 1-based coordinates
     kstring_t str = {0,0,0};
-    if ( from==0 ) from = 1;
-    if ( to==0 ) to = HTS_POS_MAX;
+    if ( !from ) from = 1;
+    else from++;
+#ifndef MAX_CSI_COOR
+#define MAX_CSI_COOR ((1LL << (14 + 30)) - 1)
+#endif
+    if ( to==0 ) to = MAX_CSI_COOR - 1;
+    else to++;
     ksprintf(&str,"%s:%"PRIhts_pos"-%"PRIhts_pos,line,from,to);
-    bcf_sr_set_regions(args->files,line,0);
+    bcf_sr_set_opt(args->files,BCF_SR_REGIONS_OVERLAP,args->regions_overlap);
+    bcf_sr_set_regions(args->files,str.s,0);
     free(str.s);
 
     if ( tmp_ptr ) *tmp_ptr = tmp;
@@ -773,6 +797,19 @@ static void apply_variant(args_t *args, bcf1_t *rec)
     else if ( (var_type & VCF_OTHER) && !strncasecmp(rec->d.allele[ialt],"<INS",4) ) trim_beg = 1;
 
     // Overlapping variant?
+    if ( ialt==0 && rec->pos <= args->fa_frz_pos && rec->pos + rec->rlen - 1 > args->fa_frz_pos )
+    {
+        // Applying the reference allele which overlaps a previous deletion. If we are here, it
+        // means it goes beyond the freezed position, hence the record can be trimmed and moved
+        // forward
+        int ntrim = args->fa_frz_pos - rec->pos + 1;
+        int nref  = strlen(rec->d.allele[0]);
+        assert( ntrim < nref );
+        rec->pos  += ntrim;
+        rec->rlen -= ntrim;
+        memmove(rec->d.allele[0],rec->d.allele[0]+ntrim,nref-ntrim);
+        rec->d.allele[0][nref-ntrim] = 0;
+    }
     if ( rec->pos <= args->fa_frz_pos )
     {
         // Can be still OK iff this is an insertion (and which does not follow another insertion, see #888).
@@ -787,18 +824,44 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             fprintf(stderr,"The site %s:%"PRId64" overlaps with another variant, skipping...\n", bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1);
             return;
         }
-
     }
 
+    char *ref_allele = rec->d.allele[0];
     char *alt_allele = rec->d.allele[ialt];
     int rmme_alt = 0;
 
     int len_diff = 0, alen = 0;
-    int idx = rec->pos - args->fa_ori_pos + args->fa_mod_off;
+    int idx = rec->pos - args->fa_ori_pos + args->fa_mod_off;   // position of the variant within the modified fasta sequence
     if ( idx<0 )
     {
-        fprintf(stderr,"Warning: ignoring overlapping variant starting at %s:%"PRId64"\n", bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1);
-        return;
+        if ( alt_allele[0]=='<' )   // symbolic allele
+        {
+            rec->pos  -= idx + 1;
+            rec->rlen += idx + 1;
+            idx = -1;
+        }
+        else if ( strlen(ref_allele) < -idx )   // the ref allele is shorter but overlaps the fa sequence? This should never happen
+        {
+            assert(0);
+            fprintf(stderr,"Warning: ignoring overlapping variant starting at %s:%"PRId64"\n", bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1);
+            return;
+        }
+        else if ( strlen(alt_allele) > -idx )  // the ref allele overlaps the fa and so does the alt allele
+        {
+            rec->pos   -= idx;
+            rec->rlen  += idx;
+            ref_allele -= idx;
+            alt_allele -= idx;
+            idx = 0;
+        }
+        else    // the ref allele overlaps the fa but alt allele does not: trim to leave one base before
+        {
+            rec->pos   -= idx + 1;
+            rec->rlen  += idx + 1;
+            ref_allele -= idx + 1;
+            alt_allele += strlen(alt_allele) - 1;
+            idx = -1;
+        }
     }
     if ( rec->rlen > args->fa_buf.l - idx )
     {
@@ -813,8 +876,9 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             }
         }
     }
-    if ( idx>=args->fa_buf.l )
-        error("FIXME: %s:%"PRId64" .. idx=%d, ori_pos=%d, len=%"PRIu64", off=%d\n",bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1,idx,args->fa_ori_pos,(uint64_t)args->fa_buf.l,args->fa_mod_off);
+
+    // the variant is beyond the available fasta sequence
+    if ( idx>0 && idx>=args->fa_buf.l ) return;
 
     // sanity check the reference base
     if ( alt_allele[0]=='<' )
@@ -828,7 +892,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
         if ( !strcasecmp(alt_allele,"<DEL>") )
         {
             static int multibase_ref_del_warned = 0;
-            if ( rec->d.allele[0][1]!=0 && !multibase_ref_del_warned )
+            if ( ref_allele[1]!=0 && !multibase_ref_del_warned )
             {
                 fprintf(stderr,
                     "Warning: one REF base is expected with <DEL>, assuming the actual deletion starts at POS+1 at %s:%"PRId64".\n"
@@ -837,7 +901,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             }
             if ( args->mark_del )   // insert dashes instead of delete sequence
             {
-                alt_allele = mark_del(rec->d.allele[0], rec->rlen, NULL, args->mark_del);
+                alt_allele = mark_del(ref_allele, rec->rlen, NULL, args->mark_del);
                 alen = rec->rlen;
                 len_diff = 0;
                 rmme_alt = 1;
@@ -845,7 +909,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             else
             {
                 len_diff = 1-rec->rlen;
-                alt_allele = rec->d.allele[0];     // according to VCF spec, the first REF base must precede the event
+                alt_allele = ref_allele;     // according to VCF spec, the first REF base must precede the event
                 alen = 1;
             }
         }
@@ -856,7 +920,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             return;
         }
     }
-    else if ( strncasecmp(rec->d.allele[0],args->fa_buf.s+idx,rec->rlen) )
+    else if ( idx>=0 && strncasecmp(ref_allele,args->fa_buf.s+idx,rec->rlen) )
     {
         // This is hacky, handle a special case: if SNP or an insert follows a deletion (AAC>A, C>CAA),
         // the reference base in fa_buf is lost and the check fails. We do not keep a buffer
@@ -864,10 +928,10 @@ static void apply_variant(args_t *args, bcf1_t *rec)
         // one base overlap
 
         int fail = 1;
-        if ( args->prev_base_pos==rec->pos && toupper(rec->d.allele[0][0])==toupper(args->prev_base) )
+        if ( args->prev_base_pos==rec->pos && toupper(ref_allele[0])==toupper(args->prev_base) )
         {
             if ( rec->rlen==1 ) fail = 0;
-            else if ( !strncasecmp(rec->d.allele[0]+1,args->fa_buf.s+idx+1,rec->rlen-1) ) fail = 0;
+            else if ( !strncasecmp(ref_allele+1,args->fa_buf.s+idx+1,rec->rlen-1) ) fail = 0;
         }
 
         if ( fail )
@@ -883,7 +947,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
                     "   REF .vcf: [%s]\n"
                     "   ALT .vcf: [%s]\n"
                     "   REF .fa : [%s]%c%s\n",
-                    bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1, rec->d.allele[0], alt_allele, args->fa_buf.s+idx,
+                    bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1, ref_allele, alt_allele, args->fa_buf.s+idx,
                     tmp?tmp:' ',tmp?args->fa_buf.s+idx+rec->rlen+1:""
                  );
         }
@@ -892,7 +956,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
 
         if ( args->mark_del && len_diff<0 )
         {
-            alt_allele = mark_del(rec->d.allele[0], rec->rlen, alt_allele, args->mark_del);
+            alt_allele = mark_del(ref_allele, rec->rlen, alt_allele, args->mark_del);
             alen = rec->rlen;
             len_diff = 0;
             rmme_alt = 1;
@@ -905,23 +969,24 @@ static void apply_variant(args_t *args, bcf1_t *rec)
 
         if ( args->mark_del && len_diff<0 )
         {
-            alt_allele = mark_del(rec->d.allele[0], rec->rlen, alt_allele, args->mark_del);
+            alt_allele = mark_del(ref_allele, rec->rlen, alt_allele, args->mark_del);
             alen = rec->rlen;
             len_diff = 0;
             rmme_alt = 1;
         }
     }
 
-    args->fa_case = toupper(args->fa_buf.s[idx])==args->fa_buf.s[idx] ? TO_UPPER : TO_LOWER;
+    int safe_idx = idx<0 ? 0 : idx; // idx can be negative in case of overlapping deletion
+    args->fa_case = toupper(args->fa_buf.s[safe_idx])==args->fa_buf.s[safe_idx] ? TO_UPPER : TO_LOWER;
     if ( args->fa_case==TO_UPPER )
         for (i=0; i<alen; i++) alt_allele[i] = toupper(alt_allele[i]);
     else
         for (i=0; i<alen; i++) alt_allele[i] = tolower(alt_allele[i]);
 
     if ( args->mark_ins && len_diff>0 )
-        mark_ins(rec->d.allele[0], alt_allele, args->mark_ins);
+        mark_ins(ref_allele, alt_allele, args->mark_ins);
     if ( args->mark_snv )
-        mark_snv(rec->d.allele[0], alt_allele, args->mark_snv);
+        mark_snv(ref_allele, alt_allele, args->mark_snv);
 
     if ( len_diff <= 0 )
     {
@@ -947,13 +1012,13 @@ static void apply_variant(args_t *args, bcf1_t *rec)
         ks_resize(&args->fa_buf, args->fa_buf.l + len_diff);
         memmove(args->fa_buf.s + idx + rec->rlen + len_diff, args->fa_buf.s + idx + rec->rlen, args->fa_buf.l - idx - rec->rlen);
 
-        // This can get tricky, make sure the bases unchanged by the insertion do not overwrite preceeding variants.
+        // This can get tricky, make sure the bases unchanged by the insertion do not overwrite preceding variants.
         // For example, here we want to get TAA:
         //      POS REF ALT
         //      1   C   T
         //      1   C   CAA
         int ibeg = 0;
-        while ( ibeg<alen && rec->d.allele[0][ibeg]==alt_allele[ibeg] && rec->pos + ibeg <= args->prev_base_pos  ) ibeg++;
+        while ( ibeg<alen && ref_allele[ibeg]==alt_allele[ibeg] && rec->pos + ibeg <= args->prev_base_pos  ) ibeg++;
         for (i=ibeg; i<alen; i++)
             args->fa_buf.s[idx+i] = alt_allele[i];
 
@@ -962,7 +1027,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
     if (args->chain && len_diff != 0)
     {
         // If first nucleotide of both REF and ALT are the same... (indels typically include the nucleotide before the variant)
-        if ( strncasecmp(rec->d.allele[0],alt_allele,1) == 0)
+        if ( strncasecmp(ref_allele,alt_allele,1) == 0)
         {
             // ...extend the block by 1 bp: start is 1 bp further and alleles are 1 bp shorter
             push_chain_gap(args->chain, rec->pos + 1, rec->rlen - 1, rec->pos + 1 + args->fa_mod_off, alen - 1);
@@ -1135,6 +1200,7 @@ static void usage(args_t *args)
     fprintf(stderr, "    -M, --missing CHAR             Output CHAR instead of skipping a missing genotype \"./.\"\n");
     fprintf(stderr, "    -o, --output FILE              Write output to a file [standard output]\n");
     fprintf(stderr, "    -p, --prefix STRING            Prefix to add to output sequence names\n");
+    fprintf(stderr, "        --regions-overlap 0|1|2    Include if POS in the region (0), record overlaps (1), variant overlaps (2) [1]\n");
     fprintf(stderr, "    -s, --samples LIST             Comma-separated list of samples to include, \"-\" to ignore samples and use REF,ALT\n");
     fprintf(stderr, "    -S, --samples-file FILE        File of samples to include\n");
     fprintf(stderr, "Examples:\n");
@@ -1151,6 +1217,7 @@ int main_consensus(int argc, char *argv[])
 {
     args_t *args = (args_t*) calloc(1,sizeof(args_t));
     args->argc   = argc; args->argv = argv;
+    args->regions_overlap = 1;
 
     static struct option loptions[] =
     {
@@ -1172,6 +1239,7 @@ int main_consensus(int argc, char *argv[])
         {"absent",1,0,'a'},
         {"chain",1,0,'c'},
         {"prefix",required_argument,0,'p'},
+        {"regions-overlap",required_argument,0,5},
         {0,0,0,0}
     };
     int c;
@@ -1192,6 +1260,10 @@ int main_consensus(int argc, char *argv[])
                 else if ( !optarg[1] && optarg[0]>32 && optarg[0]<127 ) args->mark_snv = optarg[0];
                 else error("The argument is not recognised: --mark-snv %s\n",optarg);
                 break;
+            case  5 :
+                args->regions_overlap = parse_overlap_option(optarg);
+                if ( args->regions_overlap < 0 ) error("Could not parse: --regions-overlap %s\n",optarg);
+                break;
             case 'p': args->chr_prefix = optarg; break;
             case 's': args->sample = optarg; break;
             case 'S': args->sample_fname = optarg; break;
index 9f0826b715cc46720a2794bbdecc9e581ae340ef..a004f00418e219f260aaa516b5e0c1f9cd8d76f4 100644 (file)
@@ -2,7 +2,7 @@
 
 /* The MIT License
 
-   Copyright (c) 2014-2023 Genome Research Ltd.
+   Copyright (c) 2014-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -120,7 +120,7 @@ typedef struct
     char **argv;
     int argc, output_iupac, iupac_GTs, haplotype, allele, isample, napplied;
     uint8_t *iupac_bitmask, *iupac_als;
-    int miupac_bitmask, miupac_als;
+    int miupac_bitmask, miupac_als, regions_overlap;
     char *fname, *ref_fname, *sample, *sample_fname, *output_fname, *mask_fname, *chain_fname, missing_allele, absent_allele;
     char mark_del, mark_ins, mark_snv;
     smpl_ilist_t *smpl;
@@ -231,7 +231,14 @@ static void init_data(args_t *args)
     args->hdr = args->files->readers[0].header;
     args->isample = -1;
     if ( !args->sample )
+    {
         args->smpl = smpl_ilist_init(args->hdr,NULL,0,SMPL_NONE|SMPL_VERBOSE);
+        if ( !args->smpl->n )
+        {
+            smpl_ilist_destroy(args->smpl);
+            args->smpl = NULL;
+        }
+    }
     else if ( args->sample && strcmp("-",args->sample) )
     {
         args->smpl = smpl_ilist_init(args->hdr,args->sample,0,SMPL_NONE|SMPL_VERBOSE);
@@ -246,12 +253,22 @@ static void init_data(args_t *args)
     {
         if ( args->haplotype || args->allele )
         {
-            if ( args->smpl->n > 1 ) error("Too many samples, only one can be used with -H\n");
+            if ( args->smpl->n > 1 ) error("Too many samples, only one can be used with -H; check the -s,-S options\n");
             args->isample = args->smpl->idx[0];
         }
         else
+        {
             args->iupac_GTs = 1;
+            if ( args->smpl->n==1 )
+                fprintf(bcftools_stderr,"Note: applying IUPAC codes based on FORMAT/GT in sample %s\n",bcf_hdr_int2id(args->hdr,BCF_DT_SAMPLE,args->smpl->idx[0]));
+            else
+                fprintf(bcftools_stderr,"Note: applying IUPAC codes based on FORMAT/GT in %d samples\n",args->smpl->n);
+        }
     }
+    else if ( args->output_iupac )
+        fprintf(bcftools_stderr,"Note: applying IUPAC codes based on REF,ALT%s\n",bcf_hdr_nsamples(args->hdr)?", ignoring samples":"");
+    else
+        fprintf(bcftools_stderr,"Note: applying REF,ALT variants%s\n",bcf_hdr_nsamples(args->hdr)?", ignoring samples":"");
     int i;
     for (i=0; i<args->nmask; i++)
     {
@@ -274,7 +291,6 @@ static void init_data(args_t *args)
         if ( ! args->fp_out ) error("Failed to create %s: %s\n", args->output_fname, strerror(errno));
     }
     else args->fp_out = bcftools_stdout;
-    if ( args->isample<0 && !args->iupac_GTs ) fprintf(bcftools_stderr,"Note: the --samples option not given, applying all records regardless of the genotype\n");
     if ( args->filter_str )
         args->filter = filter_init(args->hdr, args->filter_str);
     args->rid = -1;
@@ -350,20 +366,28 @@ static void init_region(args_t *args, char *line)
     args->prev_base_pos = -1;
     args->fa_buf.l  = 0;
     args->fa_length = 0;
-    args->fa_end_pos = to;
-    args->fa_ori_pos = from;
-    args->fa_src_pos = from;
+    args->fa_end_pos = to;      // 0-based
+    args->fa_ori_pos = from;    // 0-based
+    args->fa_src_pos = from;    // 0-based
     args->fa_mod_off = 0;
     args->fa_frz_pos = -1;
     args->fa_frz_mod = -1;
     args->fa_case    = -1;
     args->vcf_rbuf.n = 0;
 
+
+    // bcf_sr_set_regions accepts 1-based coordinates
     kstring_t str = {0,0,0};
-    if ( from==0 ) from = 1;
-    if ( to==0 ) to = HTS_POS_MAX;
+    if ( !from ) from = 1;
+    else from++;
+#ifndef MAX_CSI_COOR
+#define MAX_CSI_COOR ((1LL << (14 + 30)) - 1)
+#endif
+    if ( to==0 ) to = MAX_CSI_COOR - 1;
+    else to++;
     ksprintf(&str,"%s:%"PRIhts_pos"-%"PRIhts_pos,line,from,to);
-    bcf_sr_set_regions(args->files,line,0);
+    bcf_sr_set_opt(args->files,BCF_SR_REGIONS_OVERLAP,args->regions_overlap);
+    bcf_sr_set_regions(args->files,str.s,0);
     free(str.s);
 
     if ( tmp_ptr ) *tmp_ptr = tmp;
@@ -775,6 +799,19 @@ static void apply_variant(args_t *args, bcf1_t *rec)
     else if ( (var_type & VCF_OTHER) && !strncasecmp(rec->d.allele[ialt],"<INS",4) ) trim_beg = 1;
 
     // Overlapping variant?
+    if ( ialt==0 && rec->pos <= args->fa_frz_pos && rec->pos + rec->rlen - 1 > args->fa_frz_pos )
+    {
+        // Applying the reference allele which overlaps a previous deletion. If we are here, it
+        // means it goes beyond the freezed position, hence the record can be trimmed and moved
+        // forward
+        int ntrim = args->fa_frz_pos - rec->pos + 1;
+        int nref  = strlen(rec->d.allele[0]);
+        assert( ntrim < nref );
+        rec->pos  += ntrim;
+        rec->rlen -= ntrim;
+        memmove(rec->d.allele[0],rec->d.allele[0]+ntrim,nref-ntrim);
+        rec->d.allele[0][nref-ntrim] = 0;
+    }
     if ( rec->pos <= args->fa_frz_pos )
     {
         // Can be still OK iff this is an insertion (and which does not follow another insertion, see #888).
@@ -789,18 +826,44 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             fprintf(bcftools_stderr,"The site %s:%"PRId64" overlaps with another variant, skipping...\n", bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1);
             return;
         }
-
     }
 
+    char *ref_allele = rec->d.allele[0];
     char *alt_allele = rec->d.allele[ialt];
     int rmme_alt = 0;
 
     int len_diff = 0, alen = 0;
-    int idx = rec->pos - args->fa_ori_pos + args->fa_mod_off;
+    int idx = rec->pos - args->fa_ori_pos + args->fa_mod_off;   // position of the variant within the modified fasta sequence
     if ( idx<0 )
     {
-        fprintf(bcftools_stderr,"Warning: ignoring overlapping variant starting at %s:%"PRId64"\n", bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1);
-        return;
+        if ( alt_allele[0]=='<' )   // symbolic allele
+        {
+            rec->pos  -= idx + 1;
+            rec->rlen += idx + 1;
+            idx = -1;
+        }
+        else if ( strlen(ref_allele) < -idx )   // the ref allele is shorter but overlaps the fa sequence? This should never happen
+        {
+            assert(0);
+            fprintf(bcftools_stderr,"Warning: ignoring overlapping variant starting at %s:%"PRId64"\n", bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1);
+            return;
+        }
+        else if ( strlen(alt_allele) > -idx )  // the ref allele overlaps the fa and so does the alt allele
+        {
+            rec->pos   -= idx;
+            rec->rlen  += idx;
+            ref_allele -= idx;
+            alt_allele -= idx;
+            idx = 0;
+        }
+        else    // the ref allele overlaps the fa but alt allele does not: trim to leave one base before
+        {
+            rec->pos   -= idx + 1;
+            rec->rlen  += idx + 1;
+            ref_allele -= idx + 1;
+            alt_allele += strlen(alt_allele) - 1;
+            idx = -1;
+        }
     }
     if ( rec->rlen > args->fa_buf.l - idx )
     {
@@ -815,8 +878,9 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             }
         }
     }
-    if ( idx>=args->fa_buf.l )
-        error("FIXME: %s:%"PRId64" .. idx=%d, ori_pos=%d, len=%"PRIu64", off=%d\n",bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1,idx,args->fa_ori_pos,(uint64_t)args->fa_buf.l,args->fa_mod_off);
+
+    // the variant is beyond the available fasta sequence
+    if ( idx>0 && idx>=args->fa_buf.l ) return;
 
     // sanity check the reference base
     if ( alt_allele[0]=='<' )
@@ -830,7 +894,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
         if ( !strcasecmp(alt_allele,"<DEL>") )
         {
             static int multibase_ref_del_warned = 0;
-            if ( rec->d.allele[0][1]!=0 && !multibase_ref_del_warned )
+            if ( ref_allele[1]!=0 && !multibase_ref_del_warned )
             {
                 fprintf(bcftools_stderr,
                     "Warning: one REF base is expected with <DEL>, assuming the actual deletion starts at POS+1 at %s:%"PRId64".\n"
@@ -839,7 +903,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             }
             if ( args->mark_del )   // insert dashes instead of delete sequence
             {
-                alt_allele = mark_del(rec->d.allele[0], rec->rlen, NULL, args->mark_del);
+                alt_allele = mark_del(ref_allele, rec->rlen, NULL, args->mark_del);
                 alen = rec->rlen;
                 len_diff = 0;
                 rmme_alt = 1;
@@ -847,7 +911,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             else
             {
                 len_diff = 1-rec->rlen;
-                alt_allele = rec->d.allele[0];     // according to VCF spec, the first REF base must precede the event
+                alt_allele = ref_allele;     // according to VCF spec, the first REF base must precede the event
                 alen = 1;
             }
         }
@@ -858,7 +922,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
             return;
         }
     }
-    else if ( strncasecmp(rec->d.allele[0],args->fa_buf.s+idx,rec->rlen) )
+    else if ( idx>=0 && strncasecmp(ref_allele,args->fa_buf.s+idx,rec->rlen) )
     {
         // This is hacky, handle a special case: if SNP or an insert follows a deletion (AAC>A, C>CAA),
         // the reference base in fa_buf is lost and the check fails. We do not keep a buffer
@@ -866,10 +930,10 @@ static void apply_variant(args_t *args, bcf1_t *rec)
         // one base overlap
 
         int fail = 1;
-        if ( args->prev_base_pos==rec->pos && toupper(rec->d.allele[0][0])==toupper(args->prev_base) )
+        if ( args->prev_base_pos==rec->pos && toupper(ref_allele[0])==toupper(args->prev_base) )
         {
             if ( rec->rlen==1 ) fail = 0;
-            else if ( !strncasecmp(rec->d.allele[0]+1,args->fa_buf.s+idx+1,rec->rlen-1) ) fail = 0;
+            else if ( !strncasecmp(ref_allele+1,args->fa_buf.s+idx+1,rec->rlen-1) ) fail = 0;
         }
 
         if ( fail )
@@ -885,7 +949,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
                     "   REF .vcf: [%s]\n"
                     "   ALT .vcf: [%s]\n"
                     "   REF .fa : [%s]%c%s\n",
-                    bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1, rec->d.allele[0], alt_allele, args->fa_buf.s+idx,
+                    bcf_seqname(args->hdr,rec),(int64_t) rec->pos+1, ref_allele, alt_allele, args->fa_buf.s+idx,
                     tmp?tmp:' ',tmp?args->fa_buf.s+idx+rec->rlen+1:""
                  );
         }
@@ -894,7 +958,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
 
         if ( args->mark_del && len_diff<0 )
         {
-            alt_allele = mark_del(rec->d.allele[0], rec->rlen, alt_allele, args->mark_del);
+            alt_allele = mark_del(ref_allele, rec->rlen, alt_allele, args->mark_del);
             alen = rec->rlen;
             len_diff = 0;
             rmme_alt = 1;
@@ -907,23 +971,24 @@ static void apply_variant(args_t *args, bcf1_t *rec)
 
         if ( args->mark_del && len_diff<0 )
         {
-            alt_allele = mark_del(rec->d.allele[0], rec->rlen, alt_allele, args->mark_del);
+            alt_allele = mark_del(ref_allele, rec->rlen, alt_allele, args->mark_del);
             alen = rec->rlen;
             len_diff = 0;
             rmme_alt = 1;
         }
     }
 
-    args->fa_case = toupper(args->fa_buf.s[idx])==args->fa_buf.s[idx] ? TO_UPPER : TO_LOWER;
+    int safe_idx = idx<0 ? 0 : idx; // idx can be negative in case of overlapping deletion
+    args->fa_case = toupper(args->fa_buf.s[safe_idx])==args->fa_buf.s[safe_idx] ? TO_UPPER : TO_LOWER;
     if ( args->fa_case==TO_UPPER )
         for (i=0; i<alen; i++) alt_allele[i] = toupper(alt_allele[i]);
     else
         for (i=0; i<alen; i++) alt_allele[i] = tolower(alt_allele[i]);
 
     if ( args->mark_ins && len_diff>0 )
-        mark_ins(rec->d.allele[0], alt_allele, args->mark_ins);
+        mark_ins(ref_allele, alt_allele, args->mark_ins);
     if ( args->mark_snv )
-        mark_snv(rec->d.allele[0], alt_allele, args->mark_snv);
+        mark_snv(ref_allele, alt_allele, args->mark_snv);
 
     if ( len_diff <= 0 )
     {
@@ -949,13 +1014,13 @@ static void apply_variant(args_t *args, bcf1_t *rec)
         ks_resize(&args->fa_buf, args->fa_buf.l + len_diff);
         memmove(args->fa_buf.s + idx + rec->rlen + len_diff, args->fa_buf.s + idx + rec->rlen, args->fa_buf.l - idx - rec->rlen);
 
-        // This can get tricky, make sure the bases unchanged by the insertion do not overwrite preceeding variants.
+        // This can get tricky, make sure the bases unchanged by the insertion do not overwrite preceding variants.
         // For example, here we want to get TAA:
         //      POS REF ALT
         //      1   C   T
         //      1   C   CAA
         int ibeg = 0;
-        while ( ibeg<alen && rec->d.allele[0][ibeg]==alt_allele[ibeg] && rec->pos + ibeg <= args->prev_base_pos  ) ibeg++;
+        while ( ibeg<alen && ref_allele[ibeg]==alt_allele[ibeg] && rec->pos + ibeg <= args->prev_base_pos  ) ibeg++;
         for (i=ibeg; i<alen; i++)
             args->fa_buf.s[idx+i] = alt_allele[i];
 
@@ -964,7 +1029,7 @@ static void apply_variant(args_t *args, bcf1_t *rec)
     if (args->chain && len_diff != 0)
     {
         // If first nucleotide of both REF and ALT are the same... (indels typically include the nucleotide before the variant)
-        if ( strncasecmp(rec->d.allele[0],alt_allele,1) == 0)
+        if ( strncasecmp(ref_allele,alt_allele,1) == 0)
         {
             // ...extend the block by 1 bp: start is 1 bp further and alleles are 1 bp shorter
             push_chain_gap(args->chain, rec->pos + 1, rec->rlen - 1, rec->pos + 1 + args->fa_mod_off, alen - 1);
@@ -1137,6 +1202,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "    -M, --missing CHAR             Output CHAR instead of skipping a missing genotype \"./.\"\n");
     fprintf(bcftools_stderr, "    -o, --output FILE              Write output to a file [standard output]\n");
     fprintf(bcftools_stderr, "    -p, --prefix STRING            Prefix to add to output sequence names\n");
+    fprintf(bcftools_stderr, "        --regions-overlap 0|1|2    Include if POS in the region (0), record overlaps (1), variant overlaps (2) [1]\n");
     fprintf(bcftools_stderr, "    -s, --samples LIST             Comma-separated list of samples to include, \"-\" to ignore samples and use REF,ALT\n");
     fprintf(bcftools_stderr, "    -S, --samples-file FILE        File of samples to include\n");
     fprintf(bcftools_stderr, "Examples:\n");
@@ -1153,6 +1219,7 @@ int main_consensus(int argc, char *argv[])
 {
     args_t *args = (args_t*) calloc(1,sizeof(args_t));
     args->argc   = argc; args->argv = argv;
+    args->regions_overlap = 1;
 
     static struct option loptions[] =
     {
@@ -1174,6 +1241,7 @@ int main_consensus(int argc, char *argv[])
         {"absent",1,0,'a'},
         {"chain",1,0,'c'},
         {"prefix",required_argument,0,'p'},
+        {"regions-overlap",required_argument,0,5},
         {0,0,0,0}
     };
     int c;
@@ -1194,6 +1262,10 @@ int main_consensus(int argc, char *argv[])
                 else if ( !optarg[1] && optarg[0]>32 && optarg[0]<127 ) args->mark_snv = optarg[0];
                 else error("The argument is not recognised: --mark-snv %s\n",optarg);
                 break;
+            case  5 :
+                args->regions_overlap = parse_overlap_option(optarg);
+                if ( args->regions_overlap < 0 ) error("Could not parse: --regions-overlap %s\n",optarg);
+                break;
             case 'p': args->chr_prefix = optarg; break;
             case 's': args->sample = optarg; break;
             case 'S': args->sample_fname = optarg; break;
index 07ff018621962a5ec3f81d9a7d954f3b1ec2cb99..c459c83878a0e9821064a484f7c80d38999ad850 100644 (file)
@@ -1,6 +1,6 @@
 /*  convert.c -- functions for converting between VCF/BCF and related formats.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -31,6 +31,7 @@ THE SOFTWARE.  */
 #include <errno.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <stdint.h>
 #define __STDC_FORMAT_MACROS
 #include <inttypes.h>
 #include <math.h>
@@ -39,6 +40,7 @@ THE SOFTWARE.  */
 #include <htslib/vcfutils.h>
 #include <htslib/kfunc.h>
 #include <htslib/khash_str2int.h>
+#include <htslib/hts_endian.h>
 #include "bcftools.h"
 #include "variantkey.h"
 #include "convert.h"
@@ -104,9 +106,12 @@ struct _convert_t
     char *undef_info_tag;
     void *used_tags_hash;
     char **used_tags_list;
+    char *print_filtered;
     int nused_tags;
     int allow_undef_tags;
     int force_newline;
+    int header_samples;
+    int no_hdr_indices;
     uint8_t **subset_samples;
 };
 
@@ -172,23 +177,23 @@ static void process_filter(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isa
     }
     else kputc('.', str);
 }
-static inline int32_t bcf_array_ivalue(void *bcf_array, int type, int idx)
+static inline int32_t bcf_array_ivalue(uint8_t *bcf_array, int type, int idx)
 {
     if ( type==BCF_BT_INT8 )
     {
-        int8_t val = ((int8_t*)bcf_array)[idx];
+        int8_t val = le_to_i8(&bcf_array[idx * sizeof(val)]);
         if ( val==bcf_int8_missing ) return bcf_int32_missing;
         if ( val==bcf_int8_vector_end ) return bcf_int32_vector_end;
         return val;
     }
     if ( type==BCF_BT_INT16 )
     {
-        int16_t val = ((int16_t*)bcf_array)[idx];
+        int16_t val = le_to_i16(&bcf_array[idx * sizeof(val)]);
         if ( val==bcf_int16_missing ) return bcf_int32_missing;
         if ( val==bcf_int16_vector_end ) return bcf_int32_vector_end;
         return val;
     }
-    return ((int32_t*)bcf_array)[idx];
+    return le_to_i32(&bcf_array[idx * sizeof(int32_t)]);
 }
 static inline void _copy_field(char *src, uint32_t len, int idx, kstring_t *str)
 {
@@ -286,17 +291,17 @@ static void process_info(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isamp
             kputc('.', str);
             return;
         }
-        #define BRANCH(type_t, is_missing, is_vector_end, kprint) { \
-            type_t val = ((type_t *) info->vptr)[fmt->subscript]; \
+        #define BRANCH(type_t, convert, is_missing, is_vector_end, kprint) { \
+            type_t val = convert(&info->vptr[fmt->subscript * sizeof(type_t)]); \
             if ( is_missing || is_vector_end ) kputc('.',str); \
             else kprint; \
         }
         switch (info->type)
         {
-            case BCF_BT_INT8:  BRANCH(int8_t,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  kputw(val, str)); break;
-            case BCF_BT_INT16: BRANCH(int16_t, val==bcf_int16_missing, val==bcf_int16_vector_end, kputw(val, str)); break;
-            case BCF_BT_INT32: BRANCH(int32_t, val==bcf_int32_missing, val==bcf_int32_vector_end, kputw(val, str)); break;
-            case BCF_BT_FLOAT: BRANCH(float,   bcf_float_is_missing(val), bcf_float_is_vector_end(val), kputd(val, str)); break;
+            case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  kputw(val, str)); break;
+            case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, kputw(val, str)); break;
+            case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, kputw(val, str)); break;
+            case BCF_BT_FLOAT: BRANCH(float,   le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), kputd(val, str)); break;
             case BCF_BT_CHAR:  _copy_field((char*)info->vptr, info->vptr_len, fmt->subscript, str); break;
             default: fprintf(stderr,"todo: type %d\n", info->type); exit(1); break;
         }
@@ -384,11 +389,12 @@ static void process_format(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isa
         }
         if ( fmt->fmt->type == BCF_BT_FLOAT )
         {
-            float *ptr = (float*)(fmt->fmt->p + isample*fmt->fmt->size);
-            if ( bcf_float_is_missing(ptr[fmt->subscript]) || bcf_float_is_vector_end(ptr[fmt->subscript]) )
+            uint8_t *ptr = fmt->fmt->p + isample*fmt->fmt->size;
+            float val = le_to_float(&ptr[fmt->subscript * sizeof(float)]);
+            if ( bcf_float_is_missing(val) || bcf_float_is_vector_end(val) )
                 kputc('.', str);
             else
-                kputd(ptr[fmt->subscript], str);
+                kputd(val, str);
         }
         else if ( fmt->fmt->type != BCF_BT_CHAR )
         {
@@ -501,14 +507,14 @@ static void process_tbcsq(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isam
 
     int mask = fmt->subscript==0 ? 3 : 1;   // merge both haplotypes if subscript==0
 
-    #define BRANCH(type_t, nbits) { \
-        type_t *x = (type_t*)(fmt->fmt->p + isample*fmt->fmt->size); \
+    #define BRANCH(type_t, convert, nbits) { \
+        uint8_t *x = fmt->fmt->p + isample*fmt->fmt->size; \
         int i,j; \
         if ( fmt->subscript<=0 || fmt->subscript==1 ) \
         { \
             for (j=0; j < fmt->fmt->n; j++) \
             { \
-                type_t val = x[j]; \
+                type_t val = convert(&x[j * sizeof(type_t)]); \
                 if ( !val ) continue; \
                 for (i=0; i<nbits; i+=2) \
                     if ( val & (mask<<i) ) { kputs(csq->str[(j*30+i)/2], &csq->hap1); kputc_(',', &csq->hap1); } \
@@ -518,7 +524,7 @@ static void process_tbcsq(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isam
         { \
             for (j=0; j < fmt->fmt->n; j++) \
             { \
-                type_t val = x[j]; \
+                type_t val = convert(&x[j * sizeof(type_t)]); \
                 if ( !val ) continue; \
                 for (i=1; i<nbits; i+=2) \
                     if ( val & (1<<i) ) { kputs(csq->str[(j*30+i)/2], &csq->hap2); kputc_(',', &csq->hap2); } \
@@ -527,9 +533,9 @@ static void process_tbcsq(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isam
     }
     switch (fmt->fmt->type)
     {
-        case BCF_BT_INT8:  BRANCH(uint8_t, 8); break;
-        case BCF_BT_INT16: BRANCH(uint16_t,16); break;
-        case BCF_BT_INT32: BRANCH(uint32_t,30); break;  // 2 bytes unused to account for the reserved BCF values
+        case BCF_BT_INT8:  BRANCH(uint8_t,  le_to_u8,   8); break;
+        case BCF_BT_INT16: BRANCH(uint16_t, le_to_u16, 16); break;
+        case BCF_BT_INT32: BRANCH(uint32_t, le_to_u32, 30); break;  // 2 bits unused to account for the reserved BCF values
         default: error("Unexpected type: %d\n", fmt->fmt->type); exit(1); break;
     }
     #undef BRANCH
@@ -1185,16 +1191,16 @@ static void process_pbinom(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isa
         int al = bcf_gt_allele(gt[i]);
         if ( al > line->n_allele || al >= fmt->fmt->n ) goto invalid;
 
-        #define BRANCH(type_t, missing, vector_end) { \
-            type_t val = ((type_t *) fmt->fmt->p)[al + isample*fmt->fmt->n]; \
+        #define BRANCH(type_t, convert, missing, vector_end) { \
+            type_t val = convert(&fmt->fmt->p[(al + isample*fmt->fmt->n)*sizeof(type_t)]); \
             if ( val==missing || val==vector_end ) goto invalid; \
             else n[i] = val; \
         }
         switch (fmt->fmt->type)
         {
-            case BCF_BT_INT8:  BRANCH(int8_t,  bcf_int8_missing,  bcf_int8_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int16_t, bcf_int16_missing, bcf_int16_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, bcf_int32_missing, bcf_int32_vector_end); break;
+            case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  bcf_int8_missing,  bcf_int8_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, bcf_int16_missing, bcf_int16_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, bcf_int32_missing, bcf_int32_vector_end); break;
             default: goto invalid; break;
         }
         #undef BRANCH
@@ -1203,11 +1209,11 @@ static void process_pbinom(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isa
     if ( n[0]==n[1] ) kputc(n[0]==0 ? '.':'0', str);
     else
     {
-        double pval = n[0] < n[1] ? kf_betai(n[1], n[0] + 1, 0.5) : kf_betai(n[0], n[1] + 1, 0.5);
-        pval *= 2;
-        if ( pval>=1 ) pval = 0;     // this can happen, machine precision error, eg. kf_betai(1,0,0.5)
-        else
-            pval = -4.34294481903*log(pval);
+        double pval = calc_binom_two_sided(n[0],n[1],0.5);
+
+        // convrt to phred
+        if ( pval>=1 ) pval = 0;
+        else pval = -4.34294481903*log(pval);
         kputd(pval, str);
     }
     return;
@@ -1550,6 +1556,7 @@ void convert_destroy(convert_t *convert)
         free(convert->used_tags_list);
     }
     khash_str2int_destroy(convert->used_tags_hash);
+    free(convert->print_filtered);
     free(convert->fmt);
     free(convert->undef_info_tag);
     free(convert->dat);
@@ -1562,8 +1569,9 @@ void convert_destroy(convert_t *convert)
 int convert_header(convert_t *convert, kstring_t *str)
 {
     int i, icol = 0, l_ori = str->l;
+    bcf_hdr_t *hdr = convert->header;
 
-    // Supress the header output if LINE is present
+    // Suppress the header output if LINE is present
     for (i=0; i<convert->nfmt; i++)
         if ( convert->fmt[i].type == T_LINE ) break;
     if ( i!=convert->nfmt )
@@ -1585,6 +1593,7 @@ int convert_header(convert_t *convert, kstring_t *str)
             while ( convert->fmt[j].is_gt_field ) j++;
             for (js=0; js<convert->nsamples; js++)
             {
+                int ks = convert->samples[js];
                 for (k=i; k<j; k++)
                 {
                     if ( convert->fmt[k].type == T_SEP )
@@ -1600,10 +1609,29 @@ int convert_header(convert_t *convert, kstring_t *str)
                             }
                         }
                     }
+                    else if ( convert->header_samples )
+                    {
+                        icol++;
+                        if ( !convert->no_hdr_indices ) ksprintf(str,"[%d]",icol);
+                        ksprintf(str,"%s:%s", hdr->samples[ks], convert->fmt[k].key);
+                    }
                     else
-                        ksprintf(str, "[%d]%s", ++icol, convert->fmt[k].key);
+                    {
+                        icol++;
+                        if ( !convert->no_hdr_indices ) ksprintf(str,"[%d]",icol);
+                        ksprintf(str,"%s", convert->fmt[k].key);
+                    }
+                }
+                if ( has_fmt_newline )
+                {
+                    if ( !convert->header_samples ) break;
+
+                    // this is unfortunate: the formatting expression breaks the per-sample output into separate lines,
+                    // therefore including a sample name in the header makes no sense anymore
+                    convert->header_samples = 0;
+                    str->l = l_ori;
+                    return convert_header(convert, str);
                 }
-                if ( has_fmt_newline ) break;
             }
             i = j-1;
             continue;
@@ -1614,7 +1642,9 @@ int convert_header(convert_t *convert, kstring_t *str)
             if ( convert->fmt[i].key ) kputs(convert->fmt[i].key, str);
             continue;
         }
-        ksprintf(str, "[%d]%s", ++icol, convert->fmt[i].key);
+        icol++;
+        if ( !convert->no_hdr_indices ) ksprintf(str,"[%d]",icol);
+        ksprintf(str,"%s", convert->fmt[i].key);
     }
     if ( has_fmt_newline ) kputc('\n',str);
     return str->l - l_ori;
@@ -1653,7 +1683,17 @@ int convert_line(convert_t *convert, bcf1_t *line, kstring_t *str)
             {
                 // Skip samples when filtering was requested
                 int ks = convert->samples[js];
-                if ( convert->subset_samples && *convert->subset_samples && !(*convert->subset_samples)[ks] ) continue;
+                if ( convert->subset_samples && *convert->subset_samples && !(*convert->subset_samples)[ks] )
+                {
+                    if ( !convert->print_filtered ) continue;
+
+                    for (k=i; k<j; k++)
+                        if ( convert->fmt[k].type==T_SEP )
+                            convert->fmt[k].handler(convert, line, &convert->fmt[k], ks, str);
+                        else
+                            kputs(convert->print_filtered, str);
+                    continue;
+                }
 
                 // Here comes a hack designed for TBCSQ. When running on large files,
                 // such as 1000GP, there are too many empty fields in the output and
@@ -1709,29 +1749,18 @@ static void force_newline_(convert_t *convert)
     }
     if ( has_newline ) return;
 
-    // A newline is not present, force it. But where to add it?
-    // Consider
-    //      -f'%CHROM[ %SAMPLE]\n'
-    // vs
-    //      -f'[%CHROM %SAMPLE\n]'
-    for (i=0; i<convert->nfmt; i++)
-        if ( !convert->fmt[i].is_gt_field && convert->fmt[i].key ) break;
-
-    if ( i < convert->nfmt )
-        register_tag(convert, "\n", 0, T_SEP);  // the first case
-    else
-    {
-        // the second case
-        i = convert->nfmt - 1;
-        if ( !convert->fmt[i].key )
-        {
-            convert->fmt[i].key = strdup("\n");
-            convert->fmt[i].is_gt_field = 1;
-            register_tag(convert, NULL, 0, T_SEP);
-        }
-        else
-            register_tag(convert, "\n", 1, T_SEP);
-    }
+    // A newline is not present, force it. But where to add it? Always at the end.
+    //
+    // Briefly, in 1.18, we considered the following automatic behavior, which for
+    // per-site output it would add it at the end of the expression and for per-sample
+    // output it would add it inside the square brackets:
+    //           -f'%CHROM[ %SAMPLE]\n'
+    //           -f'[%CHROM %SAMPLE\n]'
+    //
+    // However, this is an annoyance for users, as it is not entirely clear what
+    // will happen unless one understands the internals well (#1969)
+
+    register_tag(convert, "\n", 0, T_SEP);
 }
 
 int convert_set_option(convert_t *convert, enum convert_option opt, ...)
@@ -1748,10 +1777,19 @@ int convert_set_option(convert_t *convert, enum convert_option opt, ...)
         case subset_samples:
             convert->subset_samples = va_arg(args, uint8_t**);
             break;
+        case header_samples:
+            convert->header_samples = va_arg(args, int);
+            break;
+        case print_filtered:
+            convert->print_filtered = strdup(va_arg(args, char*));
+            break;
         case force_newline:
             convert->force_newline = va_arg(args, int);
             if ( convert->force_newline ) force_newline_(convert);
             break;
+        case no_hdr_indices:
+            convert->no_hdr_indices = va_arg(args, int);
+            break;
         default:
             ret = -1;
     }
index 09a7648cc3ff5a8ed809f84be92e9dd9e1d71c90..e7d2905c2ae5501fa11f69c26db007b49ab7f0f5 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  convert.c -- functions for converting between VCF/BCF and related formats.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -33,6 +33,7 @@ THE SOFTWARE.  */
 #include <errno.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <stdint.h>
 #define __STDC_FORMAT_MACROS
 #include <inttypes.h>
 #include <math.h>
@@ -41,6 +42,7 @@ THE SOFTWARE.  */
 #include <htslib/vcfutils.h>
 #include <htslib/kfunc.h>
 #include <htslib/khash_str2int.h>
+#include <htslib/hts_endian.h>
 #include "bcftools.h"
 #include "variantkey.h"
 #include "convert.h"
@@ -106,9 +108,12 @@ struct _convert_t
     char *undef_info_tag;
     void *used_tags_hash;
     char **used_tags_list;
+    char *print_filtered;
     int nused_tags;
     int allow_undef_tags;
     int force_newline;
+    int header_samples;
+    int no_hdr_indices;
     uint8_t **subset_samples;
 };
 
@@ -174,23 +179,23 @@ static void process_filter(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isa
     }
     else kputc('.', str);
 }
-static inline int32_t bcf_array_ivalue(void *bcf_array, int type, int idx)
+static inline int32_t bcf_array_ivalue(uint8_t *bcf_array, int type, int idx)
 {
     if ( type==BCF_BT_INT8 )
     {
-        int8_t val = ((int8_t*)bcf_array)[idx];
+        int8_t val = le_to_i8(&bcf_array[idx * sizeof(val)]);
         if ( val==bcf_int8_missing ) return bcf_int32_missing;
         if ( val==bcf_int8_vector_end ) return bcf_int32_vector_end;
         return val;
     }
     if ( type==BCF_BT_INT16 )
     {
-        int16_t val = ((int16_t*)bcf_array)[idx];
+        int16_t val = le_to_i16(&bcf_array[idx * sizeof(val)]);
         if ( val==bcf_int16_missing ) return bcf_int32_missing;
         if ( val==bcf_int16_vector_end ) return bcf_int32_vector_end;
         return val;
     }
-    return ((int32_t*)bcf_array)[idx];
+    return le_to_i32(&bcf_array[idx * sizeof(int32_t)]);
 }
 static inline void _copy_field(char *src, uint32_t len, int idx, kstring_t *str)
 {
@@ -288,17 +293,17 @@ static void process_info(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isamp
             kputc('.', str);
             return;
         }
-        #define BRANCH(type_t, is_missing, is_vector_end, kprint) { \
-            type_t val = ((type_t *) info->vptr)[fmt->subscript]; \
+        #define BRANCH(type_t, convert, is_missing, is_vector_end, kprint) { \
+            type_t val = convert(&info->vptr[fmt->subscript * sizeof(type_t)]); \
             if ( is_missing || is_vector_end ) kputc('.',str); \
             else kprint; \
         }
         switch (info->type)
         {
-            case BCF_BT_INT8:  BRANCH(int8_t,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  kputw(val, str)); break;
-            case BCF_BT_INT16: BRANCH(int16_t, val==bcf_int16_missing, val==bcf_int16_vector_end, kputw(val, str)); break;
-            case BCF_BT_INT32: BRANCH(int32_t, val==bcf_int32_missing, val==bcf_int32_vector_end, kputw(val, str)); break;
-            case BCF_BT_FLOAT: BRANCH(float,   bcf_float_is_missing(val), bcf_float_is_vector_end(val), kputd(val, str)); break;
+            case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  kputw(val, str)); break;
+            case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, kputw(val, str)); break;
+            case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, kputw(val, str)); break;
+            case BCF_BT_FLOAT: BRANCH(float,   le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), kputd(val, str)); break;
             case BCF_BT_CHAR:  _copy_field((char*)info->vptr, info->vptr_len, fmt->subscript, str); break;
             default: fprintf(bcftools_stderr,"todo: type %d\n", info->type); bcftools_exit(1); break;
         }
@@ -386,11 +391,12 @@ static void process_format(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isa
         }
         if ( fmt->fmt->type == BCF_BT_FLOAT )
         {
-            float *ptr = (float*)(fmt->fmt->p + isample*fmt->fmt->size);
-            if ( bcf_float_is_missing(ptr[fmt->subscript]) || bcf_float_is_vector_end(ptr[fmt->subscript]) )
+            uint8_t *ptr = fmt->fmt->p + isample*fmt->fmt->size;
+            float val = le_to_float(&ptr[fmt->subscript * sizeof(float)]);
+            if ( bcf_float_is_missing(val) || bcf_float_is_vector_end(val) )
                 kputc('.', str);
             else
-                kputd(ptr[fmt->subscript], str);
+                kputd(val, str);
         }
         else if ( fmt->fmt->type != BCF_BT_CHAR )
         {
@@ -503,14 +509,14 @@ static void process_tbcsq(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isam
 
     int mask = fmt->subscript==0 ? 3 : 1;   // merge both haplotypes if subscript==0
 
-    #define BRANCH(type_t, nbits) { \
-        type_t *x = (type_t*)(fmt->fmt->p + isample*fmt->fmt->size); \
+    #define BRANCH(type_t, convert, nbits) { \
+        uint8_t *x = fmt->fmt->p + isample*fmt->fmt->size; \
         int i,j; \
         if ( fmt->subscript<=0 || fmt->subscript==1 ) \
         { \
             for (j=0; j < fmt->fmt->n; j++) \
             { \
-                type_t val = x[j]; \
+                type_t val = convert(&x[j * sizeof(type_t)]); \
                 if ( !val ) continue; \
                 for (i=0; i<nbits; i+=2) \
                     if ( val & (mask<<i) ) { kputs(csq->str[(j*30+i)/2], &csq->hap1); kputc_(',', &csq->hap1); } \
@@ -520,7 +526,7 @@ static void process_tbcsq(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isam
         { \
             for (j=0; j < fmt->fmt->n; j++) \
             { \
-                type_t val = x[j]; \
+                type_t val = convert(&x[j * sizeof(type_t)]); \
                 if ( !val ) continue; \
                 for (i=1; i<nbits; i+=2) \
                     if ( val & (1<<i) ) { kputs(csq->str[(j*30+i)/2], &csq->hap2); kputc_(',', &csq->hap2); } \
@@ -529,9 +535,9 @@ static void process_tbcsq(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isam
     }
     switch (fmt->fmt->type)
     {
-        case BCF_BT_INT8:  BRANCH(uint8_t, 8); break;
-        case BCF_BT_INT16: BRANCH(uint16_t,16); break;
-        case BCF_BT_INT32: BRANCH(uint32_t,30); break;  // 2 bytes unused to account for the reserved BCF values
+        case BCF_BT_INT8:  BRANCH(uint8_t,  le_to_u8,   8); break;
+        case BCF_BT_INT16: BRANCH(uint16_t, le_to_u16, 16); break;
+        case BCF_BT_INT32: BRANCH(uint32_t, le_to_u32, 30); break;  // 2 bits unused to account for the reserved BCF values
         default: error("Unexpected type: %d\n", fmt->fmt->type); bcftools_exit(1); break;
     }
     #undef BRANCH
@@ -1187,16 +1193,16 @@ static void process_pbinom(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isa
         int al = bcf_gt_allele(gt[i]);
         if ( al > line->n_allele || al >= fmt->fmt->n ) goto invalid;
 
-        #define BRANCH(type_t, missing, vector_end) { \
-            type_t val = ((type_t *) fmt->fmt->p)[al + isample*fmt->fmt->n]; \
+        #define BRANCH(type_t, convert, missing, vector_end) { \
+            type_t val = convert(&fmt->fmt->p[(al + isample*fmt->fmt->n)*sizeof(type_t)]); \
             if ( val==missing || val==vector_end ) goto invalid; \
             else n[i] = val; \
         }
         switch (fmt->fmt->type)
         {
-            case BCF_BT_INT8:  BRANCH(int8_t,  bcf_int8_missing,  bcf_int8_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int16_t, bcf_int16_missing, bcf_int16_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, bcf_int32_missing, bcf_int32_vector_end); break;
+            case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  bcf_int8_missing,  bcf_int8_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, bcf_int16_missing, bcf_int16_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, bcf_int32_missing, bcf_int32_vector_end); break;
             default: goto invalid; break;
         }
         #undef BRANCH
@@ -1205,11 +1211,11 @@ static void process_pbinom(convert_t *convert, bcf1_t *line, fmt_t *fmt, int isa
     if ( n[0]==n[1] ) kputc(n[0]==0 ? '.':'0', str);
     else
     {
-        double pval = n[0] < n[1] ? kf_betai(n[1], n[0] + 1, 0.5) : kf_betai(n[0], n[1] + 1, 0.5);
-        pval *= 2;
-        if ( pval>=1 ) pval = 0;     // this can happen, machine precision error, eg. kf_betai(1,0,0.5)
-        else
-            pval = -4.34294481903*log(pval);
+        double pval = calc_binom_two_sided(n[0],n[1],0.5);
+
+        // convrt to phred
+        if ( pval>=1 ) pval = 0;
+        else pval = -4.34294481903*log(pval);
         kputd(pval, str);
     }
     return;
@@ -1552,6 +1558,7 @@ void convert_destroy(convert_t *convert)
         free(convert->used_tags_list);
     }
     khash_str2int_destroy(convert->used_tags_hash);
+    free(convert->print_filtered);
     free(convert->fmt);
     free(convert->undef_info_tag);
     free(convert->dat);
@@ -1564,8 +1571,9 @@ void convert_destroy(convert_t *convert)
 int convert_header(convert_t *convert, kstring_t *str)
 {
     int i, icol = 0, l_ori = str->l;
+    bcf_hdr_t *hdr = convert->header;
 
-    // Supress the header output if LINE is present
+    // Suppress the header output if LINE is present
     for (i=0; i<convert->nfmt; i++)
         if ( convert->fmt[i].type == T_LINE ) break;
     if ( i!=convert->nfmt )
@@ -1587,6 +1595,7 @@ int convert_header(convert_t *convert, kstring_t *str)
             while ( convert->fmt[j].is_gt_field ) j++;
             for (js=0; js<convert->nsamples; js++)
             {
+                int ks = convert->samples[js];
                 for (k=i; k<j; k++)
                 {
                     if ( convert->fmt[k].type == T_SEP )
@@ -1602,10 +1611,29 @@ int convert_header(convert_t *convert, kstring_t *str)
                             }
                         }
                     }
+                    else if ( convert->header_samples )
+                    {
+                        icol++;
+                        if ( !convert->no_hdr_indices ) ksprintf(str,"[%d]",icol);
+                        ksprintf(str,"%s:%s", hdr->samples[ks], convert->fmt[k].key);
+                    }
                     else
-                        ksprintf(str, "[%d]%s", ++icol, convert->fmt[k].key);
+                    {
+                        icol++;
+                        if ( !convert->no_hdr_indices ) ksprintf(str,"[%d]",icol);
+                        ksprintf(str,"%s", convert->fmt[k].key);
+                    }
+                }
+                if ( has_fmt_newline )
+                {
+                    if ( !convert->header_samples ) break;
+
+                    // this is unfortunate: the formatting expression breaks the per-sample output into separate lines,
+                    // therefore including a sample name in the header makes no sense anymore
+                    convert->header_samples = 0;
+                    str->l = l_ori;
+                    return convert_header(convert, str);
                 }
-                if ( has_fmt_newline ) break;
             }
             i = j-1;
             continue;
@@ -1616,7 +1644,9 @@ int convert_header(convert_t *convert, kstring_t *str)
             if ( convert->fmt[i].key ) kputs(convert->fmt[i].key, str);
             continue;
         }
-        ksprintf(str, "[%d]%s", ++icol, convert->fmt[i].key);
+        icol++;
+        if ( !convert->no_hdr_indices ) ksprintf(str,"[%d]",icol);
+        ksprintf(str,"%s", convert->fmt[i].key);
     }
     if ( has_fmt_newline ) kputc('\n',str);
     return str->l - l_ori;
@@ -1655,7 +1685,17 @@ int convert_line(convert_t *convert, bcf1_t *line, kstring_t *str)
             {
                 // Skip samples when filtering was requested
                 int ks = convert->samples[js];
-                if ( convert->subset_samples && *convert->subset_samples && !(*convert->subset_samples)[ks] ) continue;
+                if ( convert->subset_samples && *convert->subset_samples && !(*convert->subset_samples)[ks] )
+                {
+                    if ( !convert->print_filtered ) continue;
+
+                    for (k=i; k<j; k++)
+                        if ( convert->fmt[k].type==T_SEP )
+                            convert->fmt[k].handler(convert, line, &convert->fmt[k], ks, str);
+                        else
+                            kputs(convert->print_filtered, str);
+                    continue;
+                }
 
                 // Here comes a hack designed for TBCSQ. When running on large files,
                 // such as 1000GP, there are too many empty fields in the output and
@@ -1711,29 +1751,18 @@ static void force_newline_(convert_t *convert)
     }
     if ( has_newline ) return;
 
-    // A newline is not present, force it. But where to add it?
-    // Consider
-    //      -f'%CHROM[ %SAMPLE]\n'
-    // vs
-    //      -f'[%CHROM %SAMPLE\n]'
-    for (i=0; i<convert->nfmt; i++)
-        if ( !convert->fmt[i].is_gt_field && convert->fmt[i].key ) break;
-
-    if ( i < convert->nfmt )
-        register_tag(convert, "\n", 0, T_SEP);  // the first case
-    else
-    {
-        // the second case
-        i = convert->nfmt - 1;
-        if ( !convert->fmt[i].key )
-        {
-            convert->fmt[i].key = strdup("\n");
-            convert->fmt[i].is_gt_field = 1;
-            register_tag(convert, NULL, 0, T_SEP);
-        }
-        else
-            register_tag(convert, "\n", 1, T_SEP);
-    }
+    // A newline is not present, force it. But where to add it? Always at the end.
+    //
+    // Briefly, in 1.18, we considered the following automatic behavior, which for
+    // per-site output it would add it at the end of the expression and for per-sample
+    // output it would add it inside the square brackets:
+    //           -f'%CHROM[ %SAMPLE]\n'
+    //           -f'[%CHROM %SAMPLE\n]'
+    //
+    // However, this is an annoyance for users, as it is not entirely clear what
+    // will happen unless one understands the internals well (#1969)
+
+    register_tag(convert, "\n", 0, T_SEP);
 }
 
 int convert_set_option(convert_t *convert, enum convert_option opt, ...)
@@ -1750,10 +1779,19 @@ int convert_set_option(convert_t *convert, enum convert_option opt, ...)
         case subset_samples:
             convert->subset_samples = va_arg(args, uint8_t**);
             break;
+        case header_samples:
+            convert->header_samples = va_arg(args, int);
+            break;
+        case print_filtered:
+            convert->print_filtered = strdup(va_arg(args, char*));
+            break;
         case force_newline:
             convert->force_newline = va_arg(args, int);
             if ( convert->force_newline ) force_newline_(convert);
             break;
+        case no_hdr_indices:
+            convert->no_hdr_indices = va_arg(args, int);
+            break;
         default:
             ret = -1;
     }
index 0626070937c44a61e82c71812d4e2b854cf3f4ae..150751481c197934de154db66e2d613b823f8ce6 100644 (file)
@@ -1,6 +1,6 @@
 /*  convert.h -- functions for converting between VCF/BCF and related formats.
 
-    Copyright (C) 2014-2023 Genome Research Ltd.
+    Copyright (C) 2014-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -30,9 +30,12 @@ THE SOFTWARE.  */
 typedef struct _convert_t convert_t;
 enum convert_option
 {
-    allow_undef_tags,
-    subset_samples,
-    force_newline,
+    allow_undef_tags,       // see `bcftools query --allow-undef-tags`, throws an error if tag is not defined otherwise
+    subset_samples,         // in bracketed expressions (e.g. [ %GT]) consider only marked samples
+    header_samples,         // include sample name in bracketed tags (e.g. SAMPLE1:GT SAMPLE2:GT for [ %GT])
+    force_newline,          // automatically insert a newline when not part of the formatting expression
+    print_filtered,         // print the provided string instead of discarding samples not included in subset_samples
+    no_hdr_indices,         // drop column indices when printing header, i.e. "#CHROM", not "#[1]CHROM"
 };
 
 convert_t *convert_init(bcf_hdr_t *hdr, int *samples, int nsamples, const char *str);
index f619e061add1fb638701bf1796e44296cb6fd9bc..b38eba107ec0cb6b67d4f6b89b506368efaec991 100644 (file)
@@ -1,6 +1,6 @@
 /* The MIT License
 
-   Copyright (c) 2016-2023 Genome Research Ltd.
+   Copyright (c) 2016-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -34,7 +34,7 @@
 
     Read about transcript types here
         http://vega.sanger.ac.uk/info/about/gene_and_transcript_types.html
-        http://www.ensembl.org/info/genome/variation/predicted_data.html
+        https://www.ensembl.org/info/genome/variation/prediction/predicted_data.html
         https://www.gencodegenes.org/pages/biotypes.html
 
     List of supported biotypes
 #include <errno.h>
 #include <unistd.h>
 #include <ctype.h>
+#include <strings.h>
 #include "bcftools.h"
 #include "filter.h"
 #include "regidx.h"
@@ -554,7 +555,9 @@ void init_data(args_t *args)
         if ( args->hdr_nsmpl )
             bcf_hdr_printf(args->hdr,"##FORMAT=<ID=%s,Number=.,Type=Integer,Description=\"Bitmask of indexes to INFO/BCSQ, with interleaved first/second haplotype. Use \\\"bcftools query -f'[%%CHROM\\t%%POS\\t%%SAMPLE\\t%%TBCSQ\\n]'\\\" to translate.\">",args->bcsq_tag);
         if ( bcf_hdr_write(args->out_fh, args->hdr)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->output_fname?args->output_fname:"standard output");
-        if ( args->write_index && init_index(args->out_fh,args->hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+        if ( init_index2(args->out_fh,args->hdr,args->output_fname,
+                         &args->index_fn, args->write_index) < 0 )
+            error("Error: failed to initialise index for %s\n",args->output_fname);
     }
     if ( args->verbosity > 0 ) fprintf(stderr,"Calling...\n");
 }
@@ -623,7 +626,7 @@ void destroy_data(args_t *args)
 }
 
 /*
-    The splice_* functions are for consquences around splice sites: start,stop,splice_*
+    The splice_* functions are for consequences around splice sites: start,stop,splice_*
  */
 #define SPLICE_VAR_REF 0   // ref: ACGT>ACGT, csq not applicable, skip completely
 #define SPLICE_OUTSIDE 1   // splice acceptor or similar; csq set and is done, does not overlap the region
@@ -791,7 +794,7 @@ static inline int csq_stage_utr(args_t *args, regitr_t *itr, bcf1_t *rec, uint32
 static inline void csq_stage_splice(args_t *args, bcf1_t *rec, gf_tscript_t *tr, uint32_t type, int ial)
 {
 #if XDBG
-fprintf(stderr,"csq_stage_splice %d: type=%d\n",rec->pos+1,type);
+fprintf(stderr,"csq_stage_splice %d: type=%d\n",(int)rec->pos+1,type);
 #endif
     if ( !type ) return;
     csq_t csq;
@@ -963,7 +966,7 @@ int shifted_del_synonymous(args_t *args, splice_t *splice, uint32_t ex_beg, uint
     if ( tr->strand==STRAND_FWD && splice->vcf.pos >= ex_beg + 3 ) return 0;
 
 #if XDBG
-    fprintf(stderr,"shifted_del_synonymous: %d-%d  %s\n",ex_beg,ex_end, tr->strand==STRAND_FWD?"fwd":"rev");
+    fprintf(stderr,"shifted_del_synonymous: %d-%d  %s\n",ex_beg,ex_end, tr->strand==STRAND_FWD?"fwd":(tr->strand==STRAND_REV?"rev":"unk"));
     fprintf(stderr,"   %d  ..  %s > %s\n",splice->vcf.pos+1,splice->vcf.ref,splice->vcf.alt);
 #endif
 
@@ -996,7 +999,7 @@ int shifted_del_synonymous(args_t *args, splice_t *splice, uint32_t ex_beg, uint
         while ( ptr_vcf[i] && ptr_vcf[i]==ptr_ref[i] ) i++;
         if ( ptr_vcf[i] ) return 0;       // the deleted sequence cannot be replaced
     }
-    else
+    else if ( tr->strand==STRAND_FWD )
     {
         // STRAND_FWD
         int32_t vcf_block_beg = splice->vcf.pos + ref_len - 2*ndel;        // the position of the first base of the ref block that could potentially replace the deletion
@@ -1179,7 +1182,9 @@ fprintf(stderr,"splice_csq_del: %s>%s .. ex=%d,%d  beg,end=%d,%d  tbeg,tend=%d,%
         splice->kalt.l = 0; kputsn(splice->vcf.alt + splice->tbeg, splice->vcf.alen, &splice->kalt);
         if ( (splice->ref_beg+1 < ex_beg && splice->ref_end >= ex_beg) || (splice->ref_beg+1 < ex_end && splice->ref_end >= ex_end) ) // ouch, ugly ENST00000409523/long-overlapping-del.vcf
         {
-            splice->csq |= (splice->ref_end - splice->ref_beg)%3 ? CSQ_FRAMESHIFT_VARIANT : CSQ_INFRAME_DELETION;
+            int ref_beg = splice->ref_beg + splice->kalt.l - 1;     // 0 for AAA>A, 1 for AAA>AC
+            if ( ref_beg < splice->ref_end )
+                splice->csq |= (splice->ref_end - ref_beg)%3 ? CSQ_FRAMESHIFT_VARIANT : CSQ_INFRAME_DELETION;
             return SPLICE_OVERLAP;
         }
     }
@@ -1269,13 +1274,13 @@ fprintf(stderr,"mnp: %s>%s .. ex=%d,%d  beg,end=%d,%d  tbeg,tend=%d,%d  check_ut
     {
         if ( splice->check_region_beg ) splice->csq |= CSQ_SPLICE_REGION;
         if ( splice->tr->strand==STRAND_FWD ) { if ( splice->check_start ) splice->csq |= CSQ_START_LOST; }
-        else { if ( splice->check_stop ) splice->csq |= CSQ_STOP_LOST; }
+        else if ( splice->tr->strand==STRAND_REV ) { if ( splice->check_stop ) splice->csq |= CSQ_STOP_LOST; }
     }
     if ( splice->ref_end > ex_end - 3 )
     {
         if ( splice->check_region_end ) splice->csq |= CSQ_SPLICE_REGION;
         if ( splice->tr->strand==STRAND_REV ) { if ( splice->check_start ) splice->csq |= CSQ_START_LOST; }
-        else { if ( splice->check_stop ) splice->csq |= CSQ_STOP_LOST; }
+        else if ( splice->tr->strand==STRAND_FWD ) { if ( splice->check_stop ) splice->csq |= CSQ_STOP_LOST; }
     }
     if ( splice->set_refalt )
     {
@@ -1336,17 +1341,17 @@ int hap_init(args_t *args, hap_node_t *parent, hap_node_t *child, gf_cds_t *cds,
     if ( !(tr->trim & TRIM_5PRIME) )
     {
         if ( tr->strand==STRAND_FWD ) { if ( child->icds==0 ) splice.check_start = 1; }
-        else { if ( child->icds==tr->ncds-1 ) splice.check_start = 1; }
+        else if ( tr->strand==STRAND_REV ) { if ( child->icds==tr->ncds-1 ) splice.check_start = 1; }
     }
     if ( !(tr->trim & TRIM_3PRIME) )
     {
         if ( tr->strand==STRAND_FWD ) { if ( child->icds==tr->ncds-1 ) splice.check_stop = 1; }
-        else { if ( child->icds==0 ) splice.check_stop = 1; }
+        else if ( tr->strand==STRAND_REV ) { if ( child->icds==0 ) splice.check_stop = 1; }
     }
     if ( splice.check_start )   // do not check starts in incomplete CDS, defined as not starting with M
     {
         if ( tr->strand==STRAND_FWD ) { if ( dna2aa(TSCRIPT_AUX(tr)->ref+N_REF_PAD+cds->beg-tr->beg) != 'M' ) splice.check_start = 0; }
-        else { if ( cdna2aa(TSCRIPT_AUX(tr)->ref+N_REF_PAD+cds->beg-tr->beg+cds->len-3) != 'M' ) splice.check_start = 0; }
+        else if ( tr->strand==STRAND_REV ) { if ( cdna2aa(TSCRIPT_AUX(tr)->ref+N_REF_PAD+cds->beg-tr->beg+cds->len-3) != 'M' ) splice.check_start = 0; }
     }
     if ( child->icds!=0 ) splice.check_region_beg = 1;
     if ( child->icds!=tr->ncds-1 ) splice.check_region_end = 1;
@@ -1584,7 +1589,7 @@ fprintf(stderr,"\ntranslate: %d %d %d  fill=%d  seq.l=%d\n",sbeg,rbeg,rend,fill,
             }
         }
     }
-    else    // STRAND_REV
+    else if ( strand==STRAND_REV )
     {
         // right padding - number of bases to take from ref
         npad = (seq.m - (sbeg + seq.l)) % 3;
@@ -1671,6 +1676,7 @@ fprintf(stderr,"\ntranslate: %d %d %d  fill=%d  seq.l=%d\n",sbeg,rbeg,rend,fill,
             }
         }
     }
+    else error("Should not happen: %d\n", strand);
     kputc_(0,tseq); tseq->l--;
 #if DBG
  fprintf(stderr,"    tseq: %s\n", tseq->s);
@@ -1705,7 +1711,7 @@ void tscript_splice_ref(gf_tscript_t *tr)
 int csq_push(args_t *args, csq_t *csq, bcf1_t *rec)
 {
 #if XDBG
-fprintf(stderr,"csq_push: %d .. %d\n",rec->pos+1,csq->type.type);
+fprintf(stderr,"csq_push: %d .. %d\n",(int)rec->pos+1,csq->type.type);
 #endif
     khint_t k = kh_get(pos2vbuf, args->pos2vbuf, (int)csq->pos);
     vbuf_t *vbuf = (k == kh_end(args->pos2vbuf)) ? NULL : kh_val(args->pos2vbuf, k);
@@ -1856,7 +1862,7 @@ void kput_vcsq(args_t *args, vcsq_t *csq, kstring_t *str)
     kputs(gf_type2gff_string(csq->biotype), str);
 
     if ( CSQ_PRN_STRAND(csq->type) || csq->vstr.l )
-        kputs(csq->strand==STRAND_FWD ? "|+" : "|-", str);
+        kputs(csq->strand==STRAND_FWD ? "|+" : (csq->strand==STRAND_REV ? "|-" : "|."), str);
 
     if ( csq->vstr.l )
         kputs(csq->vstr.s, str);
@@ -1880,6 +1886,7 @@ void hap_add_csq(args_t *args, hap_t *hap, hap_node_t *node, int tlen, int ibeg,
 {
     int i;
     gf_tscript_t *tr = hap->tr;
+    assert( tr->strand==STRAND_FWD || tr->strand==STRAND_REV );
     int ref_node = tr->strand==STRAND_FWD ? ibeg : iend;
     int icsq = node->ncsq_list++;
     hts_expand0(csq_t,node->ncsq_list,node->mcsq_list,node->csq_list);
@@ -2175,7 +2182,7 @@ void hap_finalize(args_t *args, hap_t *hap)
                 indel = 0;
             }
         }
-        else
+        else if ( tr->strand==STRAND_REV )
         {
             i = istack + 1, ibeg = -1;
             while ( --i > 0 )
@@ -3328,14 +3335,14 @@ static const char *usage(void)
         "       --targets-overlap 0|1|2       Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n"
         "       --threads INT                 Use multithreading with <int> worker threads [0]\n"
         "   -v, --verbose INT                 Verbosity level 0-2 [1]\n"
-        "       --write-index                 Automatically index the output files [off]\n"
+        "   -W, --write-index[=FMT]           Automatically index the output files [off]\n"
         "\n"
         "Example:\n"
-        "   bcftools csq -f hs37d5.fa -g Homo_sapiens.GRCh37.82.gff3.gz in.vcf\n"
+        "   bcftools csq -f hs37d5.fa -g Homo_sapiens.GRCh37.87.gff3.gz in.vcf\n"
         "\n"
         "   # GFF3 annotation files can be downloaded from Ensembl. e.g. for human:\n"
-        "   ftp://ftp.ensembl.org/pub/current_gff3/homo_sapiens/\n"
-        "   ftp://ftp.ensembl.org/pub/grch37/release-84/gff3/homo_sapiens/\n"
+        "   http://ftp.ensembl.org/pub/current_gff3/homo_sapiens/\n"
+        "   http://ftp.ensembl.org/pub/grch37/current/gff3/homo_sapiens/\n"
         "\n";
 }
 
@@ -3379,7 +3386,7 @@ int main_csq(int argc, char *argv[])
         {"targets-file",1,0,'T'},
         {"targets-overlap",required_argument,NULL,5},
         {"no-version",no_argument,NULL,3},
-        {"write-index",no_argument,NULL,6},
+        {"write-index",optional_argument,NULL,'W'},
         {"dump-gff",required_argument,NULL,7},
         {"unify-chr-names",required_argument,NULL,8},
         {0,0,0,0}
@@ -3388,7 +3395,7 @@ int main_csq(int argc, char *argv[])
     int regions_overlap = 1;
     int targets_overlap = 0;
     char *targets_list = NULL, *regions_list = NULL, *tmp;
-    while ((c = getopt_long(argc, argv, "?hr:R:t:T:i:e:f:o:O:g:s:S:p:qc:ln:bB:v:",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "?hr:R:t:T:i:e:f:o:O:g:s:S:p:qc:ln:bB:v:W::",loptions,NULL)) >= 0)
     {
         switch (c)
         {
@@ -3470,7 +3477,10 @@ int main_csq(int argc, char *argv[])
                 targets_overlap = parse_overlap_option(optarg);
                 if ( targets_overlap < 0 ) error("Could not parse: --targets-overlap %s\n",optarg);
                 break;
-            case  6 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case  7 : args->dump_gff = optarg; break;
             case  8 :
                 if ( !strcmp(optarg,"0") ) args->unify_chr_names = 0;
@@ -3490,7 +3500,7 @@ int main_csq(int argc, char *argv[])
     }
     else fname = argv[optind];
     if ( argc - optind>1 ) error("%s", usage());
-    if ( !args->fa_fname ) error("Missing the --fa-ref option\n");
+    if ( !args->fa_fname ) error("Missing the --fasta-ref option\n");
     if ( !args->gff_fname ) error("Missing the --gff option\n");
     args->sr = bcf_sr_init();
     if ( targets_list )
index 5f590d16fd5d4c56ad60f72e410094ba1195057f..3f482fdf76d8ab15e63f2d30cdf12790cb2fc3cf 100644 (file)
@@ -2,7 +2,7 @@
 
 /* The MIT License
 
-   Copyright (c) 2016-2023 Genome Research Ltd.
+   Copyright (c) 2016-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -36,7 +36,7 @@
 
     Read about transcript types here
         http://vega.sanger.ac.uk/info/about/gene_and_transcript_types.html
-        http://www.ensembl.org/info/genome/variation/predicted_data.html
+        https://www.ensembl.org/info/genome/variation/prediction/predicted_data.html
         https://www.gencodegenes.org/pages/biotypes.html
 
     List of supported biotypes
 #include <errno.h>
 #include <unistd.h>
 #include <ctype.h>
+#include <strings.h>
 #include "bcftools.h"
 #include "filter.h"
 #include "regidx.h"
@@ -556,7 +557,9 @@ void init_data(args_t *args)
         if ( args->hdr_nsmpl )
             bcf_hdr_printf(args->hdr,"##FORMAT=<ID=%s,Number=.,Type=Integer,Description=\"Bitmask of indexes to INFO/BCSQ, with interleaved first/second haplotype. Use \\\"bcftools query -f'[%%CHROM\\t%%POS\\t%%SAMPLE\\t%%TBCSQ\\n]'\\\" to translate.\">",args->bcsq_tag);
         if ( bcf_hdr_write(args->out_fh, args->hdr)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->output_fname?args->output_fname:"standard output");
-        if ( args->write_index && init_index(args->out_fh,args->hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+        if ( init_index2(args->out_fh,args->hdr,args->output_fname,
+                         &args->index_fn, args->write_index) < 0 )
+            error("Error: failed to initialise index for %s\n",args->output_fname);
     }
     if ( args->verbosity > 0 ) fprintf(bcftools_stderr,"Calling...\n");
 }
@@ -625,7 +628,7 @@ void destroy_data(args_t *args)
 }
 
 /*
-    The splice_* functions are for consquences around splice sites: start,stop,splice_*
+    The splice_* functions are for consequences around splice sites: start,stop,splice_*
  */
 #define SPLICE_VAR_REF 0   // ref: ACGT>ACGT, csq not applicable, skip completely
 #define SPLICE_OUTSIDE 1   // splice acceptor or similar; csq set and is done, does not overlap the region
@@ -793,7 +796,7 @@ static inline int csq_stage_utr(args_t *args, regitr_t *itr, bcf1_t *rec, uint32
 static inline void csq_stage_splice(args_t *args, bcf1_t *rec, gf_tscript_t *tr, uint32_t type, int ial)
 {
 #if XDBG
-fprintf(bcftools_stderr,"csq_stage_splice %d: type=%d\n",rec->pos+1,type);
+fprintf(bcftools_stderr,"csq_stage_splice %d: type=%d\n",(int)rec->pos+1,type);
 #endif
     if ( !type ) return;
     csq_t csq;
@@ -965,7 +968,7 @@ int shifted_del_synonymous(args_t *args, splice_t *splice, uint32_t ex_beg, uint
     if ( tr->strand==STRAND_FWD && splice->vcf.pos >= ex_beg + 3 ) return 0;
 
 #if XDBG
-    fprintf(bcftools_stderr,"shifted_del_synonymous: %d-%d  %s\n",ex_beg,ex_end, tr->strand==STRAND_FWD?"fwd":"rev");
+    fprintf(bcftools_stderr,"shifted_del_synonymous: %d-%d  %s\n",ex_beg,ex_end, tr->strand==STRAND_FWD?"fwd":(tr->strand==STRAND_REV?"rev":"unk"));
     fprintf(bcftools_stderr,"   %d  ..  %s > %s\n",splice->vcf.pos+1,splice->vcf.ref,splice->vcf.alt);
 #endif
 
@@ -998,7 +1001,7 @@ int shifted_del_synonymous(args_t *args, splice_t *splice, uint32_t ex_beg, uint
         while ( ptr_vcf[i] && ptr_vcf[i]==ptr_ref[i] ) i++;
         if ( ptr_vcf[i] ) return 0;       // the deleted sequence cannot be replaced
     }
-    else
+    else if ( tr->strand==STRAND_FWD )
     {
         // STRAND_FWD
         int32_t vcf_block_beg = splice->vcf.pos + ref_len - 2*ndel;        // the position of the first base of the ref block that could potentially replace the deletion
@@ -1181,7 +1184,9 @@ fprintf(bcftools_stderr,"splice_csq_del: %s>%s .. ex=%d,%d  beg,end=%d,%d  tbeg,
         splice->kalt.l = 0; kputsn(splice->vcf.alt + splice->tbeg, splice->vcf.alen, &splice->kalt);
         if ( (splice->ref_beg+1 < ex_beg && splice->ref_end >= ex_beg) || (splice->ref_beg+1 < ex_end && splice->ref_end >= ex_end) ) // ouch, ugly ENST00000409523/long-overlapping-del.vcf
         {
-            splice->csq |= (splice->ref_end - splice->ref_beg)%3 ? CSQ_FRAMESHIFT_VARIANT : CSQ_INFRAME_DELETION;
+            int ref_beg = splice->ref_beg + splice->kalt.l - 1;     // 0 for AAA>A, 1 for AAA>AC
+            if ( ref_beg < splice->ref_end )
+                splice->csq |= (splice->ref_end - ref_beg)%3 ? CSQ_FRAMESHIFT_VARIANT : CSQ_INFRAME_DELETION;
             return SPLICE_OVERLAP;
         }
     }
@@ -1271,13 +1276,13 @@ fprintf(bcftools_stderr,"mnp: %s>%s .. ex=%d,%d  beg,end=%d,%d  tbeg,tend=%d,%d
     {
         if ( splice->check_region_beg ) splice->csq |= CSQ_SPLICE_REGION;
         if ( splice->tr->strand==STRAND_FWD ) { if ( splice->check_start ) splice->csq |= CSQ_START_LOST; }
-        else { if ( splice->check_stop ) splice->csq |= CSQ_STOP_LOST; }
+        else if ( splice->tr->strand==STRAND_REV ) { if ( splice->check_stop ) splice->csq |= CSQ_STOP_LOST; }
     }
     if ( splice->ref_end > ex_end - 3 )
     {
         if ( splice->check_region_end ) splice->csq |= CSQ_SPLICE_REGION;
         if ( splice->tr->strand==STRAND_REV ) { if ( splice->check_start ) splice->csq |= CSQ_START_LOST; }
-        else { if ( splice->check_stop ) splice->csq |= CSQ_STOP_LOST; }
+        else if ( splice->tr->strand==STRAND_FWD ) { if ( splice->check_stop ) splice->csq |= CSQ_STOP_LOST; }
     }
     if ( splice->set_refalt )
     {
@@ -1338,17 +1343,17 @@ int hap_init(args_t *args, hap_node_t *parent, hap_node_t *child, gf_cds_t *cds,
     if ( !(tr->trim & TRIM_5PRIME) )
     {
         if ( tr->strand==STRAND_FWD ) { if ( child->icds==0 ) splice.check_start = 1; }
-        else { if ( child->icds==tr->ncds-1 ) splice.check_start = 1; }
+        else if ( tr->strand==STRAND_REV ) { if ( child->icds==tr->ncds-1 ) splice.check_start = 1; }
     }
     if ( !(tr->trim & TRIM_3PRIME) )
     {
         if ( tr->strand==STRAND_FWD ) { if ( child->icds==tr->ncds-1 ) splice.check_stop = 1; }
-        else { if ( child->icds==0 ) splice.check_stop = 1; }
+        else if ( tr->strand==STRAND_REV ) { if ( child->icds==0 ) splice.check_stop = 1; }
     }
     if ( splice.check_start )   // do not check starts in incomplete CDS, defined as not starting with M
     {
         if ( tr->strand==STRAND_FWD ) { if ( dna2aa(TSCRIPT_AUX(tr)->ref+N_REF_PAD+cds->beg-tr->beg) != 'M' ) splice.check_start = 0; }
-        else { if ( cdna2aa(TSCRIPT_AUX(tr)->ref+N_REF_PAD+cds->beg-tr->beg+cds->len-3) != 'M' ) splice.check_start = 0; }
+        else if ( tr->strand==STRAND_REV ) { if ( cdna2aa(TSCRIPT_AUX(tr)->ref+N_REF_PAD+cds->beg-tr->beg+cds->len-3) != 'M' ) splice.check_start = 0; }
     }
     if ( child->icds!=0 ) splice.check_region_beg = 1;
     if ( child->icds!=tr->ncds-1 ) splice.check_region_end = 1;
@@ -1586,7 +1591,7 @@ fprintf(bcftools_stderr,"\ntranslate: %d %d %d  fill=%d  seq.l=%d\n",sbeg,rbeg,r
             }
         }
     }
-    else    // STRAND_REV
+    else if ( strand==STRAND_REV )
     {
         // right padding - number of bases to take from ref
         npad = (seq.m - (sbeg + seq.l)) % 3;
@@ -1673,6 +1678,7 @@ fprintf(bcftools_stderr,"\ntranslate: %d %d %d  fill=%d  seq.l=%d\n",sbeg,rbeg,r
             }
         }
     }
+    else error("Should not happen: %d\n", strand);
     kputc_(0,tseq); tseq->l--;
 #if DBG
  fprintf(bcftools_stderr,"    tseq: %s\n", tseq->s);
@@ -1707,7 +1713,7 @@ void tscript_splice_ref(gf_tscript_t *tr)
 int csq_push(args_t *args, csq_t *csq, bcf1_t *rec)
 {
 #if XDBG
-fprintf(bcftools_stderr,"csq_push: %d .. %d\n",rec->pos+1,csq->type.type);
+fprintf(bcftools_stderr,"csq_push: %d .. %d\n",(int)rec->pos+1,csq->type.type);
 #endif
     khint_t k = kh_get(pos2vbuf, args->pos2vbuf, (int)csq->pos);
     vbuf_t *vbuf = (k == kh_end(args->pos2vbuf)) ? NULL : kh_val(args->pos2vbuf, k);
@@ -1858,7 +1864,7 @@ void kput_vcsq(args_t *args, vcsq_t *csq, kstring_t *str)
     kputs(gf_type2gff_string(csq->biotype), str);
 
     if ( CSQ_PRN_STRAND(csq->type) || csq->vstr.l )
-        kputs(csq->strand==STRAND_FWD ? "|+" : "|-", str);
+        kputs(csq->strand==STRAND_FWD ? "|+" : (csq->strand==STRAND_REV ? "|-" : "|."), str);
 
     if ( csq->vstr.l )
         kputs(csq->vstr.s, str);
@@ -1882,6 +1888,7 @@ void hap_add_csq(args_t *args, hap_t *hap, hap_node_t *node, int tlen, int ibeg,
 {
     int i;
     gf_tscript_t *tr = hap->tr;
+    assert( tr->strand==STRAND_FWD || tr->strand==STRAND_REV );
     int ref_node = tr->strand==STRAND_FWD ? ibeg : iend;
     int icsq = node->ncsq_list++;
     hts_expand0(csq_t,node->ncsq_list,node->mcsq_list,node->csq_list);
@@ -2177,7 +2184,7 @@ void hap_finalize(args_t *args, hap_t *hap)
                 indel = 0;
             }
         }
-        else
+        else if ( tr->strand==STRAND_REV )
         {
             i = istack + 1, ibeg = -1;
             while ( --i > 0 )
@@ -3330,14 +3337,14 @@ static const char *usage(void)
         "       --targets-overlap 0|1|2       Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n"
         "       --threads INT                 Use multithreading with <int> worker threads [0]\n"
         "   -v, --verbose INT                 Verbosity level 0-2 [1]\n"
-        "       --write-index                 Automatically index the output files [off]\n"
+        "   -W, --write-index[=FMT]           Automatically index the output files [off]\n"
         "\n"
         "Example:\n"
-        "   bcftools csq -f hs37d5.fa -g Homo_sapiens.GRCh37.82.gff3.gz in.vcf\n"
+        "   bcftools csq -f hs37d5.fa -g Homo_sapiens.GRCh37.87.gff3.gz in.vcf\n"
         "\n"
         "   # GFF3 annotation files can be downloaded from Ensembl. e.g. for human:\n"
-        "   ftp://ftp.ensembl.org/pub/current_gff3/homo_sapiens/\n"
-        "   ftp://ftp.ensembl.org/pub/grch37/release-84/gff3/homo_sapiens/\n"
+        "   http://ftp.ensembl.org/pub/current_gff3/homo_sapiens/\n"
+        "   http://ftp.ensembl.org/pub/grch37/current/gff3/homo_sapiens/\n"
         "\n";
 }
 
@@ -3381,7 +3388,7 @@ int main_csq(int argc, char *argv[])
         {"targets-file",1,0,'T'},
         {"targets-overlap",required_argument,NULL,5},
         {"no-version",no_argument,NULL,3},
-        {"write-index",no_argument,NULL,6},
+        {"write-index",optional_argument,NULL,'W'},
         {"dump-gff",required_argument,NULL,7},
         {"unify-chr-names",required_argument,NULL,8},
         {0,0,0,0}
@@ -3390,7 +3397,7 @@ int main_csq(int argc, char *argv[])
     int regions_overlap = 1;
     int targets_overlap = 0;
     char *targets_list = NULL, *regions_list = NULL, *tmp;
-    while ((c = getopt_long(argc, argv, "?hr:R:t:T:i:e:f:o:O:g:s:S:p:qc:ln:bB:v:",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "?hr:R:t:T:i:e:f:o:O:g:s:S:p:qc:ln:bB:v:W::",loptions,NULL)) >= 0)
     {
         switch (c)
         {
@@ -3472,7 +3479,10 @@ int main_csq(int argc, char *argv[])
                 targets_overlap = parse_overlap_option(optarg);
                 if ( targets_overlap < 0 ) error("Could not parse: --targets-overlap %s\n",optarg);
                 break;
-            case  6 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case  7 : args->dump_gff = optarg; break;
             case  8 :
                 if ( !strcmp(optarg,"0") ) args->unify_chr_names = 0;
@@ -3492,7 +3502,7 @@ int main_csq(int argc, char *argv[])
     }
     else fname = argv[optind];
     if ( argc - optind>1 ) error("%s", usage());
-    if ( !args->fa_fname ) error("Missing the --fa-ref option\n");
+    if ( !args->fa_fname ) error("Missing the --fasta-ref option\n");
     if ( !args->gff_fname ) error("Missing the --gff option\n");
     args->sr = bcf_sr_init();
     if ( targets_list )
diff --git a/bcftools/edlib.c b/bcftools/edlib.c
new file mode 100644 (file)
index 0000000..5421fee
--- /dev/null
@@ -0,0 +1,662 @@
+/*
+ * A cut down C translated of the C++ edlib.cpp file.
+ * Taken from edlib v0.1.0-166-g931be2b
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+
+#include "edlib.h"
+
+typedef uint64_t Word;
+static const int WORD_SIZE = 64; // Size of Word in bits
+static const Word WORD_1 = (Word)1;
+static const Word HIGH_BIT_MASK = 1LL << 63;  // 100..00
+//#define MAX_UCHAR 255
+#define MAX_UCHAR 7 // better cache usage for our data
+
+#ifndef MAX
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#endif
+
+typedef struct Block {
+    Word P;  // Pvin
+    Word M;  // Mvin
+    int score; // score of last cell in block;
+} Block;
+
+
+/**
+ * Defines equality relation on alphabet characters.
+ * By default each character is always equal only to itself, but you can also provide additional equalities.
+ */
+typedef struct EqualityDefinition {
+    bool matrix[MAX_UCHAR + 1][MAX_UCHAR + 1];
+} EqualityDefinition;
+
+static EqualityDefinition *
+CreateEqualityDefinition(const char *alphabet, int alphabet_size,
+                        const EdlibEqualityPair* additionalEqualities,
+                        const int additionalEqualitiesLength) {
+    EqualityDefinition *ed = malloc(sizeof(*ed));
+
+    for (size_t i = 0; i < alphabet_size; i++) {
+       for (size_t j = 0; j < alphabet_size; j++) {
+           ed->matrix[i][j] = (i == j);
+       }
+    }
+    if (additionalEqualities != NULL) {
+       for (int i = 0; i < additionalEqualitiesLength; i++) {
+           const char *firstTransformed = strchr(alphabet, additionalEqualities[i].first);
+           const char *secondTransformed = strchr(alphabet, additionalEqualities[i].second);
+           if (firstTransformed && alphabet_size) {
+               ed->matrix[firstTransformed - alphabet][secondTransformed - alphabet] =
+               ed->matrix[secondTransformed - alphabet][firstTransformed - alphabet]
+                   = true;
+           }
+       }
+    }
+
+    return ed;
+}
+
+/**
+ * @param a  Element from transformed sequence.
+ * @param b  Element from transformed sequence.
+ * @return True if a and b are defined as equal, false otherwise.
+ */
+static inline const /* attribute pure or const? */
+bool equalityDefinition_areEqual(const EqualityDefinition *ed, unsigned char a, unsigned char b) {
+    return ed->matrix[a][b];
+}
+
+static int myersCalcEditDistanceSemiGlobal(const Word* Peq, int W, int maxNumBlocks,
+                                           int queryLength,
+                                           const unsigned char* target, int targetLength,
+                                           int k, EdlibAlignMode mode,
+                                           int* bestScore_, int** positions_, int* numPositions_);
+
+static char *transformSequences(const char* queryOriginal, int queryLength,
+                               const char* targetOriginal, int targetLength,
+                               unsigned char** queryTransformed,
+                               unsigned char** targetTransformed,
+                                int *alphabet_size);
+
+static inline int ceilDiv(int x, int y);
+
+static inline unsigned char* createReverseCopy(const unsigned char* seq, int length);
+
+static inline Word* buildPeq(const int alphabetLength,
+                             const unsigned char* query,
+                             const int queryLength,
+                             const EqualityDefinition* equalityDefinition);
+
+
+/**
+ * Main edlib method.
+ */
+EdlibAlignResult edlibAlign(const char* const queryOriginal, const int queryLength,
+                           const char* const targetOriginal, const int targetLength,
+                           const EdlibAlignConfig config) {
+    EdlibAlignResult result;
+    result.status = EDLIB_STATUS_OK;
+    result.editDistance = -1;
+    result.endLocations = result.startLocations = NULL;
+    result.numLocations = 0;
+    result.alignment = NULL;
+    result.alignmentLength = 0;
+    result.alphabetLength = 0;
+
+    /*------------ TRANSFORM SEQUENCES AND RECOGNIZE ALPHABET -----------*/
+    unsigned char* query, * target;
+    int alphabet_size;
+    char *alphabet = transformSequences(queryOriginal, queryLength, targetOriginal, targetLength,
+                                         &query, &target, &alphabet_size);
+    result.alphabetLength = alphabet_size;
+    /*-------------------------------------------------------*/
+
+    // Handle special situation when at least one of the sequences has length 0.
+    if (queryLength == 0 || targetLength == 0) {
+        if (config.mode == EDLIB_MODE_NW) {
+            result.editDistance = MAX(queryLength, targetLength);
+            result.endLocations = malloc(sizeof(int) * 1);
+            result.endLocations[0] = targetLength - 1;
+            result.numLocations = 1;
+        } else if (config.mode == EDLIB_MODE_SHW || config.mode == EDLIB_MODE_HW) {
+            result.editDistance = queryLength;
+            result.endLocations = malloc(sizeof(int) * 1);
+            result.endLocations[0] = -1;
+            result.numLocations = 1;
+        } else {
+            result.status = EDLIB_STATUS_ERROR;
+        }
+
+        free(query);
+        free(target);
+        free(alphabet);
+        return result;
+    }
+
+    /*--------------------- INITIALIZATION ------------------*/
+    int maxNumBlocks = ceilDiv(queryLength, WORD_SIZE); // bmax in Myers
+    int W = maxNumBlocks * WORD_SIZE - queryLength; // number of redundant cells in last level blocks
+    EqualityDefinition *equalityDefinition =
+       CreateEqualityDefinition(alphabet, alphabet_size, config.additionalEqualities, config.additionalEqualitiesLength);
+    Word* Peq = buildPeq(alphabet_size, query, queryLength, equalityDefinition);
+    /*-------------------------------------------------------*/
+
+    /*------------------ MAIN CALCULATION -------------------*/
+    // TODO: Store alignment data only after k is determined? That could make things faster.
+//    int positionNW; // Used only when mode is NW.
+//    AlignmentData* alignData = NULL;
+    bool dynamicK = false;
+    int k = config.k;
+    if (k < 0) { // If valid k is not given, auto-adjust k until solution is found.
+        dynamicK = true;
+        k = WORD_SIZE; // Gives better results than smaller k.
+    }
+
+    do {
+        if (config.mode == EDLIB_MODE_HW || config.mode == EDLIB_MODE_SHW) {
+            myersCalcEditDistanceSemiGlobal(Peq, W, maxNumBlocks,
+                                            queryLength, target, targetLength,
+                                            k, config.mode, &(result.editDistance),
+                                            &(result.endLocations), &(result.numLocations));
+        } else {  // mode == EDLIB_MODE_NW
+//            myersCalcEditDistanceNW(Peq, W, maxNumBlocks,
+//                                    queryLength, target, targetLength,
+//                                    k, &(result.editDistance), &positionNW,
+//                                    false, &alignData, -1);
+        }
+        k *= 2;
+    } while(dynamicK && result.editDistance == -1);
+
+    if (result.editDistance >= 0) {  // If there is solution.
+        // If NW mode, set end location explicitly.
+        if (config.mode == EDLIB_MODE_NW) {
+            result.endLocations = malloc(sizeof(int) * 1);
+            result.endLocations[0] = targetLength - 1;
+            result.numLocations = 1;
+        }
+
+        // Find starting locations.
+        if (config.task == EDLIB_TASK_LOC || config.task == EDLIB_TASK_PATH) {
+            result.startLocations = malloc(result.numLocations * sizeof(int));
+            if (config.mode == EDLIB_MODE_HW) {  // If HW, I need to calculate start locations.
+                const unsigned char* rTarget = createReverseCopy(target, targetLength);
+                const unsigned char* rQuery  = createReverseCopy(query, queryLength);
+                // Peq for reversed query.
+                Word* rPeq = buildPeq(alphabet_size, rQuery, queryLength, equalityDefinition);
+                for (int i = 0; i < result.numLocations; i++) {
+                    int endLocation = result.endLocations[i];
+                    if (endLocation == -1) {
+                        // NOTE: Sometimes one of optimal solutions is that query starts before target, like this:
+                        //                       AAGG <- target
+                        //                   CCTT     <- query
+                        //   It will never be only optimal solution and it does not happen often, however it is
+                        //   possible and in that case end location will be -1. What should we do with that?
+                        //   Should we just skip reporting such end location, although it is a solution?
+                        //   If we do report it, what is the start location? -4? -1? Nothing?
+                        // TODO: Figure this out. This has to do in general with how we think about start
+                        //   and end locations.
+                        //   Also, we have alignment later relying on this locations to limit the space of it's
+                        //   search -> how can it do it right if these locations are negative or incorrect?
+                        result.startLocations[i] = 0;  // I put 0 for now, but it does not make much sense.
+                    } else {
+                        int bestScoreSHW, numPositionsSHW;
+                        int* positionsSHW;
+                        myersCalcEditDistanceSemiGlobal(
+                                rPeq, W, maxNumBlocks,
+                                queryLength, rTarget + targetLength - endLocation - 1, endLocation + 1,
+                                result.editDistance, EDLIB_MODE_SHW,
+                                &bestScoreSHW, &positionsSHW, &numPositionsSHW);
+                        // Taking last location as start ensures that alignment will not start with insertions
+                        // if it can start with mismatches instead.
+                        result.startLocations[i] = endLocation - positionsSHW[numPositionsSHW - 1];
+                        free(positionsSHW);
+                    }
+                }
+                free((void *)rTarget);
+                free((void *)rQuery);
+                free(rPeq);
+            } else {  // If mode is SHW or NW
+                for (int i = 0; i < result.numLocations; i++) {
+                    result.startLocations[i] = 0;
+                }
+            }
+        }
+    }
+    /*-------------------------------------------------------*/
+
+    //--- Free memory ---//
+    free(Peq);
+    free(query);
+    free(target);
+    free(alphabet);
+    free(equalityDefinition);
+//    DestroyAlignmentData(alignData);
+    //-------------------//
+
+    return result;
+}
+
+/**
+ * Build Peq table for given query and alphabet.
+ * Peq is table of dimensions alphabetLength+1 x maxNumBlocks.
+ * Bit i of Peq[s * maxNumBlocks + b] is 1 if i-th symbol from block b of query equals symbol s, otherwise it is 0.
+ * NOTICE: free returned array with free()!
+ */
+static inline Word* buildPeq(const int alphabetLength,
+                             const unsigned char* const query,
+                             const int queryLength,
+                             const EqualityDefinition* equalityDefinition) {
+    int maxNumBlocks = ceilDiv(queryLength, WORD_SIZE);
+    // table of dimensions alphabetLength+1 x maxNumBlocks. Last symbol is wildcard.
+    Word* Peq = malloc((alphabetLength + 1) * maxNumBlocks * sizeof(*Peq));
+
+    // Build Peq (1 is match, 0 is mismatch). NOTE: last column is wildcard(symbol that matches anything) with just 1s
+    // Optimised Peq building avoiding branching.
+    for (int symbol = 0; symbol < alphabetLength; symbol++) {
+        for (int b = 0; b < maxNumBlocks; b++) {
+            Word PeqW = 0;
+            for (int r = (b+1) * WORD_SIZE - 1; r >= b * WORD_SIZE; r--) {
+                PeqW = (PeqW<<1)
+                     + (r >= queryLength
+                        || equalityDefinition_areEqual(equalityDefinition,
+                                                       query[r], symbol)); 
+            }
+            Peq[symbol * maxNumBlocks + b] = PeqW;
+        }
+    }
+    {
+        int symbol = alphabetLength;
+        for (int b = 0; b < maxNumBlocks; b++) {
+            // Last symbol is wildcard, so it is all 1s
+            Peq[symbol * maxNumBlocks + b] = (Word)-1;
+        }
+    }
+
+    return Peq;
+}
+
+
+/**
+ * Returns new sequence that is reverse of given sequence.
+ * Free returned array with free()
+ */
+static inline unsigned char* createReverseCopy(const unsigned char* const seq, const int length) {
+    unsigned char* rSeq = malloc(length);
+    for (int i = 0; i < length; i++) {
+        rSeq[i] = seq[length - i - 1];
+    }
+    return rSeq;
+}
+
+/**
+ * Corresponds to Advance_Block function from Myers.
+ * Calculates one word(block), which is part of a column.
+ * Highest bit of word (one most to the left) is most bottom cell of block from column.
+ * Pv[i] and Mv[i] define vin of cell[i]: vin = cell[i] - cell[i-1].
+ * @param [in] Pv  Bitset, Pv[i] == 1 if vin is +1, otherwise Pv[i] == 0.
+ * @param [in] Mv  Bitset, Mv[i] == 1 if vin is -1, otherwise Mv[i] == 0.
+ * @param [in] Eq  Bitset, Eq[i] == 1 if match, 0 if mismatch.
+ * @param [in] hin  Will be +1, 0 or -1.
+ * @param [out] PvOut  Bitset, PvOut[i] == 1 if vout is +1, otherwise PvOut[i] == 0.
+ * @param [out] MvOut  Bitset, MvOut[i] == 1 if vout is -1, otherwise MvOut[i] == 0.
+ * @param [out] hout  Will be +1, 0 or -1.
+ */
+static inline int calculateBlock(Word Pv, Word Mv, Word Eq, const int hin,
+                                 Word *PvOut, Word *MvOut) {
+    // hin can be 1, -1 or 0.
+    // 1  -> 00...01
+    // 0  -> 00...00
+    // -1 -> 11...11 (2-complement)
+
+    Word hinIsNeg = (Word)(hin >> 2) & WORD_1; // 00...001 if hin is -1, 00...000 if 0 or 1
+
+    Word Xv = Eq | Mv;
+    // This is instruction below written using 'if': if (hin < 0) Eq |= (Word)1;
+    Eq |= hinIsNeg;
+    Word Xh = (((Eq & Pv) + Pv) ^ Pv) | Eq;
+
+    Word Ph = Mv | ~(Xh | Pv);
+    Word Mh = Pv & Xh;
+
+    int hout = 0;
+    // This is instruction below written using 'if': if (Ph & HIGH_BIT_MASK) hout = 1;
+    hout = (Ph & HIGH_BIT_MASK) >> (WORD_SIZE - 1);
+    // This is instruction below written using 'if': if (Mh & HIGH_BIT_MASK) hout = -1;
+    hout -= (Mh & HIGH_BIT_MASK) >> (WORD_SIZE - 1);
+
+    Ph <<= 1;
+    Mh <<= 1;
+
+    // This is instruction below written using 'if': if (hin < 0) Mh |= (Word)1;
+    Mh |= hinIsNeg;
+    // This is instruction below written using 'if': if (hin > 0) Ph |= (Word)1;
+    Ph |= (Word)((hin + 1) >> 1);
+
+    *PvOut = Mh | ~(Xv | Ph);
+    *MvOut = Ph & Xv;
+
+    return hout;
+}
+
+/**
+ * Does ceiling division x / y.
+ * Note: x and y must be non-negative and x + y must not overflow.
+ */
+static inline int ceilDiv(const int x, const int y) {
+    return x % y ? x / y + 1 : x / y;
+}
+
+static inline int min(const int x, const int y) {
+    return x < y ? x : y;
+}
+
+
+/**
+ * @param [in] block
+ * @return Values of cells in block, starting with bottom cell in block.
+ */
+static inline int *getBlockCellValues(const Block block) {
+    int *scores = malloc(WORD_SIZE * sizeof(*scores));
+    int score = block.score;
+    Word mask = HIGH_BIT_MASK;
+    for (int i = 0; i < WORD_SIZE - 1; i++) {
+        scores[i] = score;
+        if (block.P & mask) score--;
+        if (block.M & mask) score++;
+        mask >>= 1;
+    }
+    scores[WORD_SIZE - 1] = score;
+    return scores;
+}
+
+/**
+ * @param [in] block
+ * @param [in] k
+ * @return True if all cells in block have value larger than k, otherwise false.
+ */
+static inline bool allBlockCellsLarger(const Block block, const int k) {
+    int *scores = getBlockCellValues(block);
+    for (int i = 0; i < WORD_SIZE; i++) {
+        if (scores[i] <= k) {
+            free(scores);
+            return false;
+        }
+    }
+
+    free(scores);
+    return true;
+}
+
+
+/**
+ * Uses Myers' bit-vector algorithm to find edit distance for one of semi-global alignment methods.
+ * @param [in] Peq  Query profile.
+ * @param [in] W  Size of padding in last block.
+ *                TODO: Calculate this directly from query, instead of passing it.
+ * @param [in] maxNumBlocks  Number of blocks needed to cover the whole query.
+ *                           TODO: Calculate this directly from query, instead of passing it.
+ * @param [in] queryLength
+ * @param [in] target
+ * @param [in] targetLength
+ * @param [in] k
+ * @param [in] mode  EDLIB_MODE_HW or EDLIB_MODE_SHW
+ * @param [out] bestScore_  Edit distance.
+ * @param [out] positions_  Array of 0-indexed positions in target at which best score was found.
+                            Make sure to free this array with free().
+ * @param [out] numPositions_  Number of positions in the positions_ array.
+ * @return Status.
+ */
+static int myersCalcEditDistanceSemiGlobal(
+        const Word* const Peq, const int W, const int maxNumBlocks,
+        const int queryLength,
+        const unsigned char* const target, const int targetLength,
+        int k, const EdlibAlignMode mode,
+        int* const bestScore_, int** const positions_, int* const numPositions_) {
+    *positions_ = NULL;
+    *numPositions_ = 0;
+
+    // firstBlock is 0-based index of first block in Ukkonen band.
+    // lastBlock is 0-based index of last block in Ukkonen band.
+    int firstBlock = 0;
+    int lastBlock = min(ceilDiv(k + 1, WORD_SIZE), maxNumBlocks) - 1; // y in Myers
+    Block *bl; // Current block
+
+    Block* blocks = malloc(maxNumBlocks * sizeof(*blocks));
+
+    // For HW, solution will never be larger then queryLength.
+    if (mode == EDLIB_MODE_HW) {
+        k = min(queryLength, k);
+    }
+
+    // Each STRONG_REDUCE_NUM column is reduced in more expensive way.
+    // This gives speed up of about 2 times for small k.
+    const int STRONG_REDUCE_NUM = 2048;
+
+    // Initialize P, M and score
+    bl = blocks;
+    for (int b = 0; b <= lastBlock; b++) {
+        bl->score = (b + 1) * WORD_SIZE;
+        bl->P = (Word)(-1); // All 1s
+        bl->M = (Word)(0);
+        bl++;
+    }
+
+    int bestScore = -1;
+#define MAX_POS 100  // maximum number of positions returned.
+    int positions[MAX_POS];
+    int npositions = 0;
+    const int startHout = mode == EDLIB_MODE_HW ? 0 : 1; // If 0 then gap before query is not penalized;
+    const unsigned char* targetChar = target;
+    for (int c = 0; c < targetLength; c++) { // for each column
+        const Word* Peq_c = Peq + (*targetChar) * maxNumBlocks;
+
+        //----------------------- Calculate column -------------------------//
+        int hout = startHout;
+        bl = blocks + firstBlock;
+        Peq_c += firstBlock;
+        for (int b = firstBlock; b <= lastBlock; b++) {
+            hout = calculateBlock(bl->P, bl->M, *Peq_c, hout, &bl->P, &bl->M);
+            bl->score += hout;
+            bl++; Peq_c++;
+        }
+        bl--; Peq_c--;
+        //------------------------------------------------------------------//
+
+        //---------- Adjust number of blocks according to Ukkonen ----------//
+        if ((lastBlock < maxNumBlocks - 1) && (bl->score - hout <= k) // bl is pointing to last block
+            && ((*(Peq_c + 1) & WORD_1) || hout < 0)) { // Peq_c is pointing to last block
+            // If score of left block is not too big, calculate one more block
+            lastBlock++; bl++; Peq_c++;
+            bl->P = (Word)(-1); // All 1s
+            bl->M = (Word)(0);
+            bl->score = (bl - 1)->score - hout + WORD_SIZE + calculateBlock(bl->P, bl->M, *Peq_c, hout, &bl->P, &bl->M);
+        } else {
+            while (lastBlock >= firstBlock && bl->score >= k + WORD_SIZE) {
+                lastBlock--; bl--; Peq_c--;
+            }
+        }
+
+        // Every some columns, do some expensive but also more efficient block reducing.
+        // This is important!
+        //
+        // Reduce the band by decreasing last block if possible.
+        if (c % STRONG_REDUCE_NUM == 0) {
+            while (lastBlock >= 0 && lastBlock >= firstBlock && allBlockCellsLarger(*bl, k)) {
+                lastBlock--; bl--; Peq_c--;
+            }
+        }
+        // For HW, even if all cells are > k, there still may be solution in next
+        // column because starting conditions at upper boundary are 0.
+        // That means that first block is always candidate for solution,
+        // and we can never end calculation before last column.
+        if (mode == EDLIB_MODE_HW && lastBlock == -1) {
+            lastBlock++; bl++; Peq_c++;
+        }
+
+        // Reduce band by increasing first block if possible. Not applicable to HW.
+        if (mode != EDLIB_MODE_HW) {
+            while (firstBlock <= lastBlock && blocks[firstBlock].score >= k + WORD_SIZE) {
+                firstBlock++;
+            }
+            if (c % STRONG_REDUCE_NUM == 0) { // Do strong reduction every some blocks
+                while (firstBlock <= lastBlock && allBlockCellsLarger(blocks[firstBlock], k)) {
+                    firstBlock++;
+                }
+            }
+        }
+
+        // If band stops to exist finish
+        if (lastBlock < firstBlock) {
+            *bestScore_ = bestScore;
+            if (bestScore != -1) {
+                *positions_ = malloc(npositions * sizeof(int));
+                *numPositions_ = npositions;
+                memcpy(*positions_, positions, npositions * sizeof(int));
+            }
+            free(blocks);
+            return EDLIB_STATUS_OK;
+        }
+        //------------------------------------------------------------------//
+
+        //------------------------- Update best score ----------------------//
+        if (lastBlock == maxNumBlocks - 1) {
+            int colScore = bl->score;
+            if (colScore <= k) { // Scores > k dont have correct values (so we cannot use them), but are certainly > k.
+                // NOTE: Score that I find in column c is actually score from column c-W
+                if (bestScore == -1 || colScore <= bestScore) {
+                    if (colScore != bestScore) {
+                       npositions = 0;
+                        bestScore = colScore;
+                        // Change k so we will look only for equal or better
+                        // scores then the best found so far.
+                        k = bestScore;
+                    }
+                   if (npositions < MAX_POS)
+                       positions[npositions++] = c - W;
+                }
+            }
+        }
+        //------------------------------------------------------------------//
+
+        targetChar++;
+    }
+
+
+    // Obtain results for last W columns from last column.
+    if (lastBlock == maxNumBlocks - 1) {
+       int *blockScores = getBlockCellValues(*bl);
+        for (int i = 0; i < W; i++) {
+            int colScore = blockScores[i + 1];
+            if (colScore <= k && (bestScore == -1 || colScore <= bestScore)) {
+                if (colScore != bestScore) {
+                    npositions = 0;
+                    k = bestScore = colScore;
+                }
+               if (npositions < MAX_POS)
+                   positions[npositions++] = targetLength - W + i;
+            }
+        }
+        free(blockScores);
+    }
+
+    *bestScore_ = bestScore;
+    if (bestScore != -1) {
+        *positions_ = malloc(npositions * sizeof(int));
+        *numPositions_ = npositions;
+        memcpy(*positions_, positions, npositions * sizeof(int));
+    }
+
+    free(blocks);
+    return EDLIB_STATUS_OK;
+}
+
+
+/**
+ * Takes char query and char target, recognizes alphabet and transforms them into unsigned char sequences
+ * where elements in sequences are not any more letters of alphabet, but their index in alphabet.
+ * Most of internal edlib functions expect such transformed sequences.
+ * This function will allocate queryTransformed and targetTransformed, so make sure to free them when done.
+ * Example:
+ *   Original sequences: "ACT" and "CGT".
+ *   Alphabet would be recognized as "ACTG". Alphabet length = 4.
+ *   Transformed sequences: [0, 1, 2] and [1, 3, 2].
+ * @param [in] queryOriginal
+ * @param [in] queryLength
+ * @param [in] targetOriginal
+ * @param [in] targetLength
+ * @param [out] queryTransformed  It will contain values in range [0, alphabet length - 1].
+ * @param [out] targetTransformed  It will contain values in range [0, alphabet length - 1].
+ * @return  Alphabet as a string of unique characters, where index of each character is its value in transformed
+ *          sequences.
+ */
+static char *transformSequences(const char* const queryOriginal, const int queryLength,
+                               const char* const targetOriginal, const int targetLength,
+                               unsigned char** const queryTransformed,
+                               unsigned char** const targetTransformed,
+                                int *alphabet_size) {
+    // Alphabet is constructed from letters that are present in sequences.
+    // Each letter is assigned an ordinal number, starting from 0 up to alphabetLength - 1,
+    // and new query and target are created in which letters are replaced with their ordinal numbers.
+    // This query and target are used in all the calculations later.
+    *queryTransformed  = malloc(sizeof(unsigned char) * queryLength);
+    *targetTransformed = malloc(sizeof(unsigned char) * targetLength);
+
+    char *alphabet = malloc(MAX_UCHAR+1), *alphabet_cp = alphabet;
+
+    // Alphabet information, it is constructed on fly while transforming sequences.
+    // letterIdx[c] is index of letter c in alphabet.
+    unsigned char letterIdx[MAX_UCHAR + 1];
+    bool inAlphabet[MAX_UCHAR + 1]; // inAlphabet[c] is true if c is in alphabet
+    for (int i = 0; i < MAX_UCHAR + 1; i++) inAlphabet[i] = false;
+
+    for (int i = 0; i < queryLength; i++) {
+        unsigned char c = queryOriginal[i];
+        if (!inAlphabet[c]) {
+            inAlphabet[c] = true;
+            letterIdx[c] = alphabet_cp - alphabet;
+            *alphabet_cp++ = queryOriginal[i];
+        }
+        (*queryTransformed)[i] = letterIdx[c];
+    }
+    for (int i = 0; i < targetLength; i++) {
+        unsigned char c = targetOriginal[i];
+        if (!inAlphabet[c]) {
+            inAlphabet[c] = true;
+            letterIdx[c] = alphabet_cp - alphabet;
+            *alphabet_cp++ = targetOriginal[i];
+        }
+        (*targetTransformed)[i] = letterIdx[c];
+    }
+
+    *alphabet_size = alphabet_cp - alphabet;
+    return alphabet;
+}
+
+
+EdlibAlignConfig edlibNewAlignConfig(int k, EdlibAlignMode mode, EdlibAlignTask task,
+                                    const EdlibEqualityPair* additionalEqualities,
+                                    int additionalEqualitiesLength) {
+    EdlibAlignConfig config;
+    config.k = k;
+    config.mode = mode;
+    config.task = task;
+    config.additionalEqualities = additionalEqualities;
+    config.additionalEqualitiesLength = additionalEqualitiesLength;
+    return config;
+}
+
+EdlibAlignConfig edlibDefaultAlignConfig(void) {
+    return edlibNewAlignConfig(-1, EDLIB_MODE_NW, EDLIB_TASK_DISTANCE, NULL, 0);
+}
+
+void edlibFreeAlignResult(EdlibAlignResult result) {
+    if (result.endLocations) free(result.endLocations);
+    if (result.startLocations) free(result.startLocations);
+    if (result.alignment) free(result.alignment);
+}
diff --git a/bcftools/edlib.c.pysam.c b/bcftools/edlib.c.pysam.c
new file mode 100644 (file)
index 0000000..e5b6998
--- /dev/null
@@ -0,0 +1,664 @@
+#include "bcftools.pysam.h"
+
+/*
+ * A cut down C translated of the C++ edlib.cpp file.
+ * Taken from edlib v0.1.0-166-g931be2b
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+
+#include "edlib.h"
+
+typedef uint64_t Word;
+static const int WORD_SIZE = 64; // Size of Word in bits
+static const Word WORD_1 = (Word)1;
+static const Word HIGH_BIT_MASK = 1LL << 63;  // 100..00
+//#define MAX_UCHAR 255
+#define MAX_UCHAR 7 // better cache usage for our data
+
+#ifndef MAX
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#endif
+
+typedef struct Block {
+    Word P;  // Pvin
+    Word M;  // Mvin
+    int score; // score of last cell in block;
+} Block;
+
+
+/**
+ * Defines equality relation on alphabet characters.
+ * By default each character is always equal only to itself, but you can also provide additional equalities.
+ */
+typedef struct EqualityDefinition {
+    bool matrix[MAX_UCHAR + 1][MAX_UCHAR + 1];
+} EqualityDefinition;
+
+static EqualityDefinition *
+CreateEqualityDefinition(const char *alphabet, int alphabet_size,
+                        const EdlibEqualityPair* additionalEqualities,
+                        const int additionalEqualitiesLength) {
+    EqualityDefinition *ed = malloc(sizeof(*ed));
+
+    for (size_t i = 0; i < alphabet_size; i++) {
+       for (size_t j = 0; j < alphabet_size; j++) {
+           ed->matrix[i][j] = (i == j);
+       }
+    }
+    if (additionalEqualities != NULL) {
+       for (int i = 0; i < additionalEqualitiesLength; i++) {
+           const char *firstTransformed = strchr(alphabet, additionalEqualities[i].first);
+           const char *secondTransformed = strchr(alphabet, additionalEqualities[i].second);
+           if (firstTransformed && alphabet_size) {
+               ed->matrix[firstTransformed - alphabet][secondTransformed - alphabet] =
+               ed->matrix[secondTransformed - alphabet][firstTransformed - alphabet]
+                   = true;
+           }
+       }
+    }
+
+    return ed;
+}
+
+/**
+ * @param a  Element from transformed sequence.
+ * @param b  Element from transformed sequence.
+ * @return True if a and b are defined as equal, false otherwise.
+ */
+static inline const /* attribute pure or const? */
+bool equalityDefinition_areEqual(const EqualityDefinition *ed, unsigned char a, unsigned char b) {
+    return ed->matrix[a][b];
+}
+
+static int myersCalcEditDistanceSemiGlobal(const Word* Peq, int W, int maxNumBlocks,
+                                           int queryLength,
+                                           const unsigned char* target, int targetLength,
+                                           int k, EdlibAlignMode mode,
+                                           int* bestScore_, int** positions_, int* numPositions_);
+
+static char *transformSequences(const char* queryOriginal, int queryLength,
+                               const char* targetOriginal, int targetLength,
+                               unsigned char** queryTransformed,
+                               unsigned char** targetTransformed,
+                                int *alphabet_size);
+
+static inline int ceilDiv(int x, int y);
+
+static inline unsigned char* createReverseCopy(const unsigned char* seq, int length);
+
+static inline Word* buildPeq(const int alphabetLength,
+                             const unsigned char* query,
+                             const int queryLength,
+                             const EqualityDefinition* equalityDefinition);
+
+
+/**
+ * Main edlib method.
+ */
+EdlibAlignResult edlibAlign(const char* const queryOriginal, const int queryLength,
+                           const char* const targetOriginal, const int targetLength,
+                           const EdlibAlignConfig config) {
+    EdlibAlignResult result;
+    result.status = EDLIB_STATUS_OK;
+    result.editDistance = -1;
+    result.endLocations = result.startLocations = NULL;
+    result.numLocations = 0;
+    result.alignment = NULL;
+    result.alignmentLength = 0;
+    result.alphabetLength = 0;
+
+    /*------------ TRANSFORM SEQUENCES AND RECOGNIZE ALPHABET -----------*/
+    unsigned char* query, * target;
+    int alphabet_size;
+    char *alphabet = transformSequences(queryOriginal, queryLength, targetOriginal, targetLength,
+                                         &query, &target, &alphabet_size);
+    result.alphabetLength = alphabet_size;
+    /*-------------------------------------------------------*/
+
+    // Handle special situation when at least one of the sequences has length 0.
+    if (queryLength == 0 || targetLength == 0) {
+        if (config.mode == EDLIB_MODE_NW) {
+            result.editDistance = MAX(queryLength, targetLength);
+            result.endLocations = malloc(sizeof(int) * 1);
+            result.endLocations[0] = targetLength - 1;
+            result.numLocations = 1;
+        } else if (config.mode == EDLIB_MODE_SHW || config.mode == EDLIB_MODE_HW) {
+            result.editDistance = queryLength;
+            result.endLocations = malloc(sizeof(int) * 1);
+            result.endLocations[0] = -1;
+            result.numLocations = 1;
+        } else {
+            result.status = EDLIB_STATUS_ERROR;
+        }
+
+        free(query);
+        free(target);
+        free(alphabet);
+        return result;
+    }
+
+    /*--------------------- INITIALIZATION ------------------*/
+    int maxNumBlocks = ceilDiv(queryLength, WORD_SIZE); // bmax in Myers
+    int W = maxNumBlocks * WORD_SIZE - queryLength; // number of redundant cells in last level blocks
+    EqualityDefinition *equalityDefinition =
+       CreateEqualityDefinition(alphabet, alphabet_size, config.additionalEqualities, config.additionalEqualitiesLength);
+    Word* Peq = buildPeq(alphabet_size, query, queryLength, equalityDefinition);
+    /*-------------------------------------------------------*/
+
+    /*------------------ MAIN CALCULATION -------------------*/
+    // TODO: Store alignment data only after k is determined? That could make things faster.
+//    int positionNW; // Used only when mode is NW.
+//    AlignmentData* alignData = NULL;
+    bool dynamicK = false;
+    int k = config.k;
+    if (k < 0) { // If valid k is not given, auto-adjust k until solution is found.
+        dynamicK = true;
+        k = WORD_SIZE; // Gives better results than smaller k.
+    }
+
+    do {
+        if (config.mode == EDLIB_MODE_HW || config.mode == EDLIB_MODE_SHW) {
+            myersCalcEditDistanceSemiGlobal(Peq, W, maxNumBlocks,
+                                            queryLength, target, targetLength,
+                                            k, config.mode, &(result.editDistance),
+                                            &(result.endLocations), &(result.numLocations));
+        } else {  // mode == EDLIB_MODE_NW
+//            myersCalcEditDistanceNW(Peq, W, maxNumBlocks,
+//                                    queryLength, target, targetLength,
+//                                    k, &(result.editDistance), &positionNW,
+//                                    false, &alignData, -1);
+        }
+        k *= 2;
+    } while(dynamicK && result.editDistance == -1);
+
+    if (result.editDistance >= 0) {  // If there is solution.
+        // If NW mode, set end location explicitly.
+        if (config.mode == EDLIB_MODE_NW) {
+            result.endLocations = malloc(sizeof(int) * 1);
+            result.endLocations[0] = targetLength - 1;
+            result.numLocations = 1;
+        }
+
+        // Find starting locations.
+        if (config.task == EDLIB_TASK_LOC || config.task == EDLIB_TASK_PATH) {
+            result.startLocations = malloc(result.numLocations * sizeof(int));
+            if (config.mode == EDLIB_MODE_HW) {  // If HW, I need to calculate start locations.
+                const unsigned char* rTarget = createReverseCopy(target, targetLength);
+                const unsigned char* rQuery  = createReverseCopy(query, queryLength);
+                // Peq for reversed query.
+                Word* rPeq = buildPeq(alphabet_size, rQuery, queryLength, equalityDefinition);
+                for (int i = 0; i < result.numLocations; i++) {
+                    int endLocation = result.endLocations[i];
+                    if (endLocation == -1) {
+                        // NOTE: Sometimes one of optimal solutions is that query starts before target, like this:
+                        //                       AAGG <- target
+                        //                   CCTT     <- query
+                        //   It will never be only optimal solution and it does not happen often, however it is
+                        //   possible and in that case end location will be -1. What should we do with that?
+                        //   Should we just skip reporting such end location, although it is a solution?
+                        //   If we do report it, what is the start location? -4? -1? Nothing?
+                        // TODO: Figure this out. This has to do in general with how we think about start
+                        //   and end locations.
+                        //   Also, we have alignment later relying on this locations to limit the space of it's
+                        //   search -> how can it do it right if these locations are negative or incorrect?
+                        result.startLocations[i] = 0;  // I put 0 for now, but it does not make much sense.
+                    } else {
+                        int bestScoreSHW, numPositionsSHW;
+                        int* positionsSHW;
+                        myersCalcEditDistanceSemiGlobal(
+                                rPeq, W, maxNumBlocks,
+                                queryLength, rTarget + targetLength - endLocation - 1, endLocation + 1,
+                                result.editDistance, EDLIB_MODE_SHW,
+                                &bestScoreSHW, &positionsSHW, &numPositionsSHW);
+                        // Taking last location as start ensures that alignment will not start with insertions
+                        // if it can start with mismatches instead.
+                        result.startLocations[i] = endLocation - positionsSHW[numPositionsSHW - 1];
+                        free(positionsSHW);
+                    }
+                }
+                free((void *)rTarget);
+                free((void *)rQuery);
+                free(rPeq);
+            } else {  // If mode is SHW or NW
+                for (int i = 0; i < result.numLocations; i++) {
+                    result.startLocations[i] = 0;
+                }
+            }
+        }
+    }
+    /*-------------------------------------------------------*/
+
+    //--- Free memory ---//
+    free(Peq);
+    free(query);
+    free(target);
+    free(alphabet);
+    free(equalityDefinition);
+//    DestroyAlignmentData(alignData);
+    //-------------------//
+
+    return result;
+}
+
+/**
+ * Build Peq table for given query and alphabet.
+ * Peq is table of dimensions alphabetLength+1 x maxNumBlocks.
+ * Bit i of Peq[s * maxNumBlocks + b] is 1 if i-th symbol from block b of query equals symbol s, otherwise it is 0.
+ * NOTICE: free returned array with free()!
+ */
+static inline Word* buildPeq(const int alphabetLength,
+                             const unsigned char* const query,
+                             const int queryLength,
+                             const EqualityDefinition* equalityDefinition) {
+    int maxNumBlocks = ceilDiv(queryLength, WORD_SIZE);
+    // table of dimensions alphabetLength+1 x maxNumBlocks. Last symbol is wildcard.
+    Word* Peq = malloc((alphabetLength + 1) * maxNumBlocks * sizeof(*Peq));
+
+    // Build Peq (1 is match, 0 is mismatch). NOTE: last column is wildcard(symbol that matches anything) with just 1s
+    // Optimised Peq building avoiding branching.
+    for (int symbol = 0; symbol < alphabetLength; symbol++) {
+        for (int b = 0; b < maxNumBlocks; b++) {
+            Word PeqW = 0;
+            for (int r = (b+1) * WORD_SIZE - 1; r >= b * WORD_SIZE; r--) {
+                PeqW = (PeqW<<1)
+                     + (r >= queryLength
+                        || equalityDefinition_areEqual(equalityDefinition,
+                                                       query[r], symbol)); 
+            }
+            Peq[symbol * maxNumBlocks + b] = PeqW;
+        }
+    }
+    {
+        int symbol = alphabetLength;
+        for (int b = 0; b < maxNumBlocks; b++) {
+            // Last symbol is wildcard, so it is all 1s
+            Peq[symbol * maxNumBlocks + b] = (Word)-1;
+        }
+    }
+
+    return Peq;
+}
+
+
+/**
+ * Returns new sequence that is reverse of given sequence.
+ * Free returned array with free()
+ */
+static inline unsigned char* createReverseCopy(const unsigned char* const seq, const int length) {
+    unsigned char* rSeq = malloc(length);
+    for (int i = 0; i < length; i++) {
+        rSeq[i] = seq[length - i - 1];
+    }
+    return rSeq;
+}
+
+/**
+ * Corresponds to Advance_Block function from Myers.
+ * Calculates one word(block), which is part of a column.
+ * Highest bit of word (one most to the left) is most bottom cell of block from column.
+ * Pv[i] and Mv[i] define vin of cell[i]: vin = cell[i] - cell[i-1].
+ * @param [in] Pv  Bitset, Pv[i] == 1 if vin is +1, otherwise Pv[i] == 0.
+ * @param [in] Mv  Bitset, Mv[i] == 1 if vin is -1, otherwise Mv[i] == 0.
+ * @param [in] Eq  Bitset, Eq[i] == 1 if match, 0 if mismatch.
+ * @param [in] hin  Will be +1, 0 or -1.
+ * @param [out] PvOut  Bitset, PvOut[i] == 1 if vout is +1, otherwise PvOut[i] == 0.
+ * @param [out] MvOut  Bitset, MvOut[i] == 1 if vout is -1, otherwise MvOut[i] == 0.
+ * @param [out] hout  Will be +1, 0 or -1.
+ */
+static inline int calculateBlock(Word Pv, Word Mv, Word Eq, const int hin,
+                                 Word *PvOut, Word *MvOut) {
+    // hin can be 1, -1 or 0.
+    // 1  -> 00...01
+    // 0  -> 00...00
+    // -1 -> 11...11 (2-complement)
+
+    Word hinIsNeg = (Word)(hin >> 2) & WORD_1; // 00...001 if hin is -1, 00...000 if 0 or 1
+
+    Word Xv = Eq | Mv;
+    // This is instruction below written using 'if': if (hin < 0) Eq |= (Word)1;
+    Eq |= hinIsNeg;
+    Word Xh = (((Eq & Pv) + Pv) ^ Pv) | Eq;
+
+    Word Ph = Mv | ~(Xh | Pv);
+    Word Mh = Pv & Xh;
+
+    int hout = 0;
+    // This is instruction below written using 'if': if (Ph & HIGH_BIT_MASK) hout = 1;
+    hout = (Ph & HIGH_BIT_MASK) >> (WORD_SIZE - 1);
+    // This is instruction below written using 'if': if (Mh & HIGH_BIT_MASK) hout = -1;
+    hout -= (Mh & HIGH_BIT_MASK) >> (WORD_SIZE - 1);
+
+    Ph <<= 1;
+    Mh <<= 1;
+
+    // This is instruction below written using 'if': if (hin < 0) Mh |= (Word)1;
+    Mh |= hinIsNeg;
+    // This is instruction below written using 'if': if (hin > 0) Ph |= (Word)1;
+    Ph |= (Word)((hin + 1) >> 1);
+
+    *PvOut = Mh | ~(Xv | Ph);
+    *MvOut = Ph & Xv;
+
+    return hout;
+}
+
+/**
+ * Does ceiling division x / y.
+ * Note: x and y must be non-negative and x + y must not overflow.
+ */
+static inline int ceilDiv(const int x, const int y) {
+    return x % y ? x / y + 1 : x / y;
+}
+
+static inline int min(const int x, const int y) {
+    return x < y ? x : y;
+}
+
+
+/**
+ * @param [in] block
+ * @return Values of cells in block, starting with bottom cell in block.
+ */
+static inline int *getBlockCellValues(const Block block) {
+    int *scores = malloc(WORD_SIZE * sizeof(*scores));
+    int score = block.score;
+    Word mask = HIGH_BIT_MASK;
+    for (int i = 0; i < WORD_SIZE - 1; i++) {
+        scores[i] = score;
+        if (block.P & mask) score--;
+        if (block.M & mask) score++;
+        mask >>= 1;
+    }
+    scores[WORD_SIZE - 1] = score;
+    return scores;
+}
+
+/**
+ * @param [in] block
+ * @param [in] k
+ * @return True if all cells in block have value larger than k, otherwise false.
+ */
+static inline bool allBlockCellsLarger(const Block block, const int k) {
+    int *scores = getBlockCellValues(block);
+    for (int i = 0; i < WORD_SIZE; i++) {
+        if (scores[i] <= k) {
+            free(scores);
+            return false;
+        }
+    }
+
+    free(scores);
+    return true;
+}
+
+
+/**
+ * Uses Myers' bit-vector algorithm to find edit distance for one of semi-global alignment methods.
+ * @param [in] Peq  Query profile.
+ * @param [in] W  Size of padding in last block.
+ *                TODO: Calculate this directly from query, instead of passing it.
+ * @param [in] maxNumBlocks  Number of blocks needed to cover the whole query.
+ *                           TODO: Calculate this directly from query, instead of passing it.
+ * @param [in] queryLength
+ * @param [in] target
+ * @param [in] targetLength
+ * @param [in] k
+ * @param [in] mode  EDLIB_MODE_HW or EDLIB_MODE_SHW
+ * @param [out] bestScore_  Edit distance.
+ * @param [out] positions_  Array of 0-indexed positions in target at which best score was found.
+                            Make sure to free this array with free().
+ * @param [out] numPositions_  Number of positions in the positions_ array.
+ * @return Status.
+ */
+static int myersCalcEditDistanceSemiGlobal(
+        const Word* const Peq, const int W, const int maxNumBlocks,
+        const int queryLength,
+        const unsigned char* const target, const int targetLength,
+        int k, const EdlibAlignMode mode,
+        int* const bestScore_, int** const positions_, int* const numPositions_) {
+    *positions_ = NULL;
+    *numPositions_ = 0;
+
+    // firstBlock is 0-based index of first block in Ukkonen band.
+    // lastBlock is 0-based index of last block in Ukkonen band.
+    int firstBlock = 0;
+    int lastBlock = min(ceilDiv(k + 1, WORD_SIZE), maxNumBlocks) - 1; // y in Myers
+    Block *bl; // Current block
+
+    Block* blocks = malloc(maxNumBlocks * sizeof(*blocks));
+
+    // For HW, solution will never be larger then queryLength.
+    if (mode == EDLIB_MODE_HW) {
+        k = min(queryLength, k);
+    }
+
+    // Each STRONG_REDUCE_NUM column is reduced in more expensive way.
+    // This gives speed up of about 2 times for small k.
+    const int STRONG_REDUCE_NUM = 2048;
+
+    // Initialize P, M and score
+    bl = blocks;
+    for (int b = 0; b <= lastBlock; b++) {
+        bl->score = (b + 1) * WORD_SIZE;
+        bl->P = (Word)(-1); // All 1s
+        bl->M = (Word)(0);
+        bl++;
+    }
+
+    int bestScore = -1;
+#define MAX_POS 100  // maximum number of positions returned.
+    int positions[MAX_POS];
+    int npositions = 0;
+    const int startHout = mode == EDLIB_MODE_HW ? 0 : 1; // If 0 then gap before query is not penalized;
+    const unsigned char* targetChar = target;
+    for (int c = 0; c < targetLength; c++) { // for each column
+        const Word* Peq_c = Peq + (*targetChar) * maxNumBlocks;
+
+        //----------------------- Calculate column -------------------------//
+        int hout = startHout;
+        bl = blocks + firstBlock;
+        Peq_c += firstBlock;
+        for (int b = firstBlock; b <= lastBlock; b++) {
+            hout = calculateBlock(bl->P, bl->M, *Peq_c, hout, &bl->P, &bl->M);
+            bl->score += hout;
+            bl++; Peq_c++;
+        }
+        bl--; Peq_c--;
+        //------------------------------------------------------------------//
+
+        //---------- Adjust number of blocks according to Ukkonen ----------//
+        if ((lastBlock < maxNumBlocks - 1) && (bl->score - hout <= k) // bl is pointing to last block
+            && ((*(Peq_c + 1) & WORD_1) || hout < 0)) { // Peq_c is pointing to last block
+            // If score of left block is not too big, calculate one more block
+            lastBlock++; bl++; Peq_c++;
+            bl->P = (Word)(-1); // All 1s
+            bl->M = (Word)(0);
+            bl->score = (bl - 1)->score - hout + WORD_SIZE + calculateBlock(bl->P, bl->M, *Peq_c, hout, &bl->P, &bl->M);
+        } else {
+            while (lastBlock >= firstBlock && bl->score >= k + WORD_SIZE) {
+                lastBlock--; bl--; Peq_c--;
+            }
+        }
+
+        // Every some columns, do some expensive but also more efficient block reducing.
+        // This is important!
+        //
+        // Reduce the band by decreasing last block if possible.
+        if (c % STRONG_REDUCE_NUM == 0) {
+            while (lastBlock >= 0 && lastBlock >= firstBlock && allBlockCellsLarger(*bl, k)) {
+                lastBlock--; bl--; Peq_c--;
+            }
+        }
+        // For HW, even if all cells are > k, there still may be solution in next
+        // column because starting conditions at upper boundary are 0.
+        // That means that first block is always candidate for solution,
+        // and we can never end calculation before last column.
+        if (mode == EDLIB_MODE_HW && lastBlock == -1) {
+            lastBlock++; bl++; Peq_c++;
+        }
+
+        // Reduce band by increasing first block if possible. Not applicable to HW.
+        if (mode != EDLIB_MODE_HW) {
+            while (firstBlock <= lastBlock && blocks[firstBlock].score >= k + WORD_SIZE) {
+                firstBlock++;
+            }
+            if (c % STRONG_REDUCE_NUM == 0) { // Do strong reduction every some blocks
+                while (firstBlock <= lastBlock && allBlockCellsLarger(blocks[firstBlock], k)) {
+                    firstBlock++;
+                }
+            }
+        }
+
+        // If band stops to exist finish
+        if (lastBlock < firstBlock) {
+            *bestScore_ = bestScore;
+            if (bestScore != -1) {
+                *positions_ = malloc(npositions * sizeof(int));
+                *numPositions_ = npositions;
+                memcpy(*positions_, positions, npositions * sizeof(int));
+            }
+            free(blocks);
+            return EDLIB_STATUS_OK;
+        }
+        //------------------------------------------------------------------//
+
+        //------------------------- Update best score ----------------------//
+        if (lastBlock == maxNumBlocks - 1) {
+            int colScore = bl->score;
+            if (colScore <= k) { // Scores > k dont have correct values (so we cannot use them), but are certainly > k.
+                // NOTE: Score that I find in column c is actually score from column c-W
+                if (bestScore == -1 || colScore <= bestScore) {
+                    if (colScore != bestScore) {
+                       npositions = 0;
+                        bestScore = colScore;
+                        // Change k so we will look only for equal or better
+                        // scores then the best found so far.
+                        k = bestScore;
+                    }
+                   if (npositions < MAX_POS)
+                       positions[npositions++] = c - W;
+                }
+            }
+        }
+        //------------------------------------------------------------------//
+
+        targetChar++;
+    }
+
+
+    // Obtain results for last W columns from last column.
+    if (lastBlock == maxNumBlocks - 1) {
+       int *blockScores = getBlockCellValues(*bl);
+        for (int i = 0; i < W; i++) {
+            int colScore = blockScores[i + 1];
+            if (colScore <= k && (bestScore == -1 || colScore <= bestScore)) {
+                if (colScore != bestScore) {
+                    npositions = 0;
+                    k = bestScore = colScore;
+                }
+               if (npositions < MAX_POS)
+                   positions[npositions++] = targetLength - W + i;
+            }
+        }
+        free(blockScores);
+    }
+
+    *bestScore_ = bestScore;
+    if (bestScore != -1) {
+        *positions_ = malloc(npositions * sizeof(int));
+        *numPositions_ = npositions;
+        memcpy(*positions_, positions, npositions * sizeof(int));
+    }
+
+    free(blocks);
+    return EDLIB_STATUS_OK;
+}
+
+
+/**
+ * Takes char query and char target, recognizes alphabet and transforms them into unsigned char sequences
+ * where elements in sequences are not any more letters of alphabet, but their index in alphabet.
+ * Most of internal edlib functions expect such transformed sequences.
+ * This function will allocate queryTransformed and targetTransformed, so make sure to free them when done.
+ * Example:
+ *   Original sequences: "ACT" and "CGT".
+ *   Alphabet would be recognized as "ACTG". Alphabet length = 4.
+ *   Transformed sequences: [0, 1, 2] and [1, 3, 2].
+ * @param [in] queryOriginal
+ * @param [in] queryLength
+ * @param [in] targetOriginal
+ * @param [in] targetLength
+ * @param [out] queryTransformed  It will contain values in range [0, alphabet length - 1].
+ * @param [out] targetTransformed  It will contain values in range [0, alphabet length - 1].
+ * @return  Alphabet as a string of unique characters, where index of each character is its value in transformed
+ *          sequences.
+ */
+static char *transformSequences(const char* const queryOriginal, const int queryLength,
+                               const char* const targetOriginal, const int targetLength,
+                               unsigned char** const queryTransformed,
+                               unsigned char** const targetTransformed,
+                                int *alphabet_size) {
+    // Alphabet is constructed from letters that are present in sequences.
+    // Each letter is assigned an ordinal number, starting from 0 up to alphabetLength - 1,
+    // and new query and target are created in which letters are replaced with their ordinal numbers.
+    // This query and target are used in all the calculations later.
+    *queryTransformed  = malloc(sizeof(unsigned char) * queryLength);
+    *targetTransformed = malloc(sizeof(unsigned char) * targetLength);
+
+    char *alphabet = malloc(MAX_UCHAR+1), *alphabet_cp = alphabet;
+
+    // Alphabet information, it is constructed on fly while transforming sequences.
+    // letterIdx[c] is index of letter c in alphabet.
+    unsigned char letterIdx[MAX_UCHAR + 1];
+    bool inAlphabet[MAX_UCHAR + 1]; // inAlphabet[c] is true if c is in alphabet
+    for (int i = 0; i < MAX_UCHAR + 1; i++) inAlphabet[i] = false;
+
+    for (int i = 0; i < queryLength; i++) {
+        unsigned char c = queryOriginal[i];
+        if (!inAlphabet[c]) {
+            inAlphabet[c] = true;
+            letterIdx[c] = alphabet_cp - alphabet;
+            *alphabet_cp++ = queryOriginal[i];
+        }
+        (*queryTransformed)[i] = letterIdx[c];
+    }
+    for (int i = 0; i < targetLength; i++) {
+        unsigned char c = targetOriginal[i];
+        if (!inAlphabet[c]) {
+            inAlphabet[c] = true;
+            letterIdx[c] = alphabet_cp - alphabet;
+            *alphabet_cp++ = targetOriginal[i];
+        }
+        (*targetTransformed)[i] = letterIdx[c];
+    }
+
+    *alphabet_size = alphabet_cp - alphabet;
+    return alphabet;
+}
+
+
+EdlibAlignConfig edlibNewAlignConfig(int k, EdlibAlignMode mode, EdlibAlignTask task,
+                                    const EdlibEqualityPair* additionalEqualities,
+                                    int additionalEqualitiesLength) {
+    EdlibAlignConfig config;
+    config.k = k;
+    config.mode = mode;
+    config.task = task;
+    config.additionalEqualities = additionalEqualities;
+    config.additionalEqualitiesLength = additionalEqualitiesLength;
+    return config;
+}
+
+EdlibAlignConfig edlibDefaultAlignConfig(void) {
+    return edlibNewAlignConfig(-1, EDLIB_MODE_NW, EDLIB_TASK_DISTANCE, NULL, 0);
+}
+
+void edlibFreeAlignResult(EdlibAlignResult result) {
+    if (result.endLocations) free(result.endLocations);
+    if (result.startLocations) free(result.startLocations);
+    if (result.alignment) free(result.alignment);
+}
diff --git a/bcftools/edlib.h b/bcftools/edlib.h
new file mode 100644 (file)
index 0000000..1f5eca1
--- /dev/null
@@ -0,0 +1,277 @@
+#ifndef EDLIB_H
+#define EDLIB_H
+
+/**
+ * @file
+ * @author Martin Sosic
+ * @brief Main header file, containing all public functions and structures.
+ */
+
+// Define EDLIB_API macro to properly export symbols
+#ifdef EDLIB_SHARED
+#    ifdef _WIN32
+#        ifdef EDLIB_BUILD
+#            define EDLIB_API __declspec(dllexport)
+#        else
+#            define EDLIB_API __declspec(dllimport)
+#        endif
+#    else
+#        define EDLIB_API __attribute__ ((visibility ("default")))
+#    endif
+#else
+#    define EDLIB_API
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Status codes
+#define EDLIB_STATUS_OK 0
+#define EDLIB_STATUS_ERROR 1
+
+    /**
+     * Alignment methods - how should Edlib treat gaps before and after query?
+     */
+    typedef enum {
+        /**
+         * Global method. This is the standard method.
+         * Useful when you want to find out how similar is first sequence to second sequence.
+         */
+        EDLIB_MODE_NW,
+        /**
+         * Prefix method. Similar to global method, but with a small twist - gap at query end is not penalized.
+         * What that means is that deleting elements from the end of second sequence is "free"!
+         * For example, if we had "AACT" and "AACTGGC", edit distance would be 0, because removing "GGC" from the end
+         * of second sequence is "free" and does not count into total edit distance. This method is appropriate
+         * when you want to find out how well first sequence fits at the beginning of second sequence.
+         */
+        EDLIB_MODE_SHW,
+        /**
+         * Infix method. Similar as prefix method, but with one more twist - gaps at query end and start are
+         * not penalized. What that means is that deleting elements from the start and end of second sequence is "free"!
+         * For example, if we had ACT and CGACTGAC, edit distance would be 0, because removing CG from the start
+         * and GAC from the end of second sequence is "free" and does not count into total edit distance.
+         * This method is appropriate when you want to find out how well first sequence fits at any part of
+         * second sequence.
+         * For example, if your second sequence was a long text and your first sequence was a sentence from that text,
+         * but slightly scrambled, you could use this method to discover how scrambled it is and where it fits in
+         * that text. In bioinformatics, this method is appropriate for aligning read to a sequence.
+         */
+        EDLIB_MODE_HW
+    } EdlibAlignMode;
+
+    /**
+     * Alignment tasks - what do you want Edlib to do?
+     */
+    typedef enum {
+        EDLIB_TASK_DISTANCE,  //!< Find edit distance and end locations.
+        EDLIB_TASK_LOC,       //!< Find edit distance, end locations and start locations.
+        EDLIB_TASK_PATH       //!< Find edit distance, end locations and start locations and alignment path.
+    } EdlibAlignTask;
+
+    /**
+     * Describes cigar format.
+     * @see http://samtools.github.io/hts-specs/SAMv1.pdf
+     * @see http://drive5.com/usearch/manual/cigar.html
+     */
+    typedef enum {
+        EDLIB_CIGAR_STANDARD,  //!< Match: 'M', Insertion: 'I', Deletion: 'D', Mismatch: 'M'.
+        EDLIB_CIGAR_EXTENDED   //!< Match: '=', Insertion: 'I', Deletion: 'D', Mismatch: 'X'.
+    } EdlibCigarFormat;
+
+// Edit operations.
+#define EDLIB_EDOP_MATCH 0    //!< Match.
+#define EDLIB_EDOP_INSERT 1   //!< Insertion to target = deletion from query.
+#define EDLIB_EDOP_DELETE 2   //!< Deletion from target = insertion to query.
+#define EDLIB_EDOP_MISMATCH 3 //!< Mismatch.
+
+    /**
+     * @brief Defines two given characters as equal.
+     */
+    typedef struct {
+        char first;
+        char second;
+    } EdlibEqualityPair;
+
+    /**
+     * @brief Configuration object for edlibAlign() function.
+     */
+    typedef struct {
+        /**
+         * Set k to non-negative value to tell edlib that edit distance is not larger than k.
+         * Smaller k can significantly improve speed of computation.
+         * If edit distance is larger than k, edlib will set edit distance to -1.
+         * Set k to negative value and edlib will internally auto-adjust k until score is found.
+         */
+        int k;
+
+        /**
+         * Alignment method.
+         * EDLIB_MODE_NW: global (Needleman-Wunsch)
+         * EDLIB_MODE_SHW: prefix. Gap after query is not penalized.
+         * EDLIB_MODE_HW: infix. Gaps before and after query are not penalized.
+         */
+        EdlibAlignMode mode;
+
+        /**
+         * Alignment task - tells Edlib what to calculate. Less to calculate, faster it is.
+         * EDLIB_TASK_DISTANCE - find edit distance and end locations of optimal alignment paths in target.
+         * EDLIB_TASK_LOC - find edit distance and start and end locations of optimal alignment paths in target.
+         * EDLIB_TASK_PATH - find edit distance, alignment path (and start and end locations of it in target).
+         */
+        EdlibAlignTask task;
+
+        /**
+         * List of pairs of characters, where each pair defines two characters as equal.
+         * This way you can extend edlib's definition of equality (which is that each character is equal only
+         * to itself).
+         * This can be useful if you have some wildcard characters that should match multiple other characters,
+         * or e.g. if you want edlib to be case insensitive.
+         * Can be set to NULL if there are none.
+         */
+        const EdlibEqualityPair* additionalEqualities;
+
+        /**
+         * Number of additional equalities, which is non-negative number.
+         * 0 if there are none.
+         */
+        int additionalEqualitiesLength;
+    } EdlibAlignConfig;
+
+    /**
+     * Helper method for easy construction of configuration object.
+     * @return Configuration object filled with given parameters.
+     */
+    EDLIB_API EdlibAlignConfig edlibNewAlignConfig(
+        int k, EdlibAlignMode mode, EdlibAlignTask task,
+        const EdlibEqualityPair* additionalEqualities,
+        int additionalEqualitiesLength
+    );
+
+    /**
+     * @return Default configuration object, with following defaults:
+     *         k = -1, mode = EDLIB_MODE_NW, task = EDLIB_TASK_DISTANCE, no additional equalities.
+     */
+    EDLIB_API EdlibAlignConfig edlibDefaultAlignConfig(void);
+
+
+    /**
+     * Container for results of alignment done by edlibAlign() function.
+     */
+    typedef struct {
+        /**
+         * EDLIB_STATUS_OK or EDLIB_STATUS_ERROR. If error, all other fields will have undefined values.
+         */
+        int status;
+
+        /**
+         * -1 if k is non-negative and edit distance is larger than k.
+         */
+        int editDistance;
+
+        /**
+         * Array of zero-based positions in target where optimal alignment paths end.
+         * If gap after query is penalized, gap counts as part of query (NW), otherwise not.
+         * Set to NULL if edit distance is larger than k.
+         * If you do not free whole result object using edlibFreeAlignResult(), do not forget to use free().
+         */
+        int* endLocations;
+
+        /**
+         * Array of zero-based positions in target where optimal alignment paths start,
+         * they correspond to endLocations.
+         * If gap before query is penalized, gap counts as part of query (NW), otherwise not.
+         * Set to NULL if not calculated or if edit distance is larger than k.
+         * If you do not free whole result object using edlibFreeAlignResult(), do not forget to use free().
+         */
+        int* startLocations;
+
+        /**
+         * Number of end (and start) locations.
+         */
+        int numLocations;
+
+        /**
+         * Alignment is found for first pair of start and end locations.
+         * Set to NULL if not calculated.
+         * Alignment is sequence of numbers: 0, 1, 2, 3.
+         * 0 stands for match.
+         * 1 stands for insertion to target.
+         * 2 stands for insertion to query.
+         * 3 stands for mismatch.
+         * Alignment aligns query to target from beginning of query till end of query.
+         * If gaps are not penalized, they are not in alignment.
+         * If you do not free whole result object using edlibFreeAlignResult(), do not forget to use free().
+         */
+        unsigned char* alignment;
+
+        /**
+         * Length of alignment.
+         */
+        int alignmentLength;
+
+        /**
+         * Number of different characters in query and target together.
+         */
+        int alphabetLength;
+    } EdlibAlignResult;
+
+    /**
+     * Frees memory in EdlibAlignResult that was allocated by edlib.
+     * If you do not use it, make sure to free needed members manually using free().
+     */
+    EDLIB_API void edlibFreeAlignResult(EdlibAlignResult result);
+
+
+    /**
+     * Aligns two sequences (query and target) using edit distance (levenshtein distance).
+     * Through config parameter, this function supports different alignment methods (global, prefix, infix),
+     * as well as different modes of search (tasks).
+     * It always returns edit distance and end locations of optimal alignment in target.
+     * It optionally returns start locations of optimal alignment in target and alignment path,
+     * if you choose appropriate tasks.
+     * @param [in] query  First sequence.
+     * @param [in] queryLength  Number of characters in first sequence.
+     * @param [in] target  Second sequence.
+     * @param [in] targetLength  Number of characters in second sequence.
+     * @param [in] config  Additional alignment parameters, like alignment method and wanted results.
+     * @return  Result of alignment, which can contain edit distance, start and end locations and alignment path.
+     *          Make sure to clean up the object using edlibFreeAlignResult() or by manually freeing needed members.
+     */
+    EDLIB_API EdlibAlignResult edlibAlign(
+        const char* query, int queryLength,
+        const char* target, int targetLength,
+        const EdlibAlignConfig config
+    );
+
+
+    /**
+     * Builds cigar string from given alignment sequence.
+     * @param [in] alignment  Alignment sequence.
+     *     0 stands for match.
+     *     1 stands for insertion to target.
+     *     2 stands for insertion to query.
+     *     3 stands for mismatch.
+     * @param [in] alignmentLength
+     * @param [in] cigarFormat  Cigar will be returned in specified format.
+     * @return Cigar string.
+     *     I stands for insertion.
+     *     D stands for deletion.
+     *     X stands for mismatch. (used only in extended format)
+     *     = stands for match. (used only in extended format)
+     *     M stands for (mis)match. (used only in standard format)
+     *     String is null terminated.
+     *     Needed memory is allocated and given pointer is set to it.
+     *     Do not forget to free it later using free()!
+     */
+    EDLIB_API char* edlibAlignmentToCigar(
+        const unsigned char* alignment, int alignmentLength,
+        EdlibCigarFormat cigarFormat
+    );
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // EDLIB_H
index b6547f81f25745ccef9c9707ec28233d27e0107c..c9dcd023b78fce174c03e9eb6b6ae81e8a63dac5 100644 (file)
@@ -1,6 +1,6 @@
 /*  filter.c -- filter expressions.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -29,6 +29,7 @@ THE SOFTWARE.  */
 #include <errno.h>
 #include <math.h>
 #include <sys/types.h>
+#include <stdint.h>
 #include <inttypes.h>
 #ifndef _WIN32
 #include <pwd.h>
@@ -38,11 +39,17 @@ THE SOFTWARE.  */
 #include <htslib/hts_defs.h>
 #include <htslib/vcfutils.h>
 #include <htslib/kfunc.h>
+#include <htslib/hts_endian.h>
 #include "config.h"
 #include "filter.h"
 #include "bcftools.h"
 
 #if ENABLE_PERL_FILTERS
+// Work around clang warning problems
+#  if defined(__clang__)
+#    define PERL_GCC_BRACE_GROUPS_FORBIDDEN
+#  endif
+
 #  define filter_t perl_filter_t
 #  include <EXTERN.h>
 #  include <perl.h>
@@ -66,7 +73,7 @@ typedef struct _token_t
     char *tag;          // for debugging and printout only, VCF tag name
     double threshold;   // filtering threshold
     int is_constant;    // the threshold is set
-    int hdr_id, tag_type;   // BCF header lookup ID and one of BCF_HL_* types
+    int hdr_id, hl_type, ht_type;   // BCF header lookup ID and one of BCF_HL_* types and BCF_HT_* types
     int idx;            // 0-based index to VCF vectors,
                         //  -2: list (e.g. [0,1,2] or [1..3] or [1..] or any field[*], which is equivalent to [0..])
                         //  -3: select indices on the fly based on values in GT
@@ -79,11 +86,12 @@ typedef struct _token_t
     void (*comparator)(struct _token_t *, struct _token_t *, struct _token_t *rtok, bcf1_t *);
     void *hash;         // test presence of str value in the hash via comparator
     regex_t *regex;     // precompiled regex for string comparison
+    int iext;           // for the use with filter_test_ext(), 1-based index to external values, 0=don't use
 
     // modified on filter evaluation at each VCF line
     double *values;
     kstring_t str_value;
-    int is_str, is_missing; // is_missing is set only for constants, variables are controled via nvalues
+    int is_str, is_missing; // is_missing is set only for constants, variables are controlled via nvalues
     int pass_site;          // -1 not applicable, 0 fails, >0 pass
     uint8_t *pass_samples;  // status of individual samples
     int nvalues, mvalues;   // number of used values: n=0 for missing values, n=1 for scalars, for strings n=str_value.l
@@ -112,6 +120,8 @@ struct _filter_t
     char **undef_tag, **used_tag;
     int nundef_tag, nused_tag;
     int status, exit_on_error;
+    int n_ext;      // number of external values to fill via filter_test_ext()
+    int *ext;       // types of external values to fill via filter_test_ext()
 };
 
 
@@ -156,13 +166,16 @@ struct _filter_t
 #define TOK_IN      38      // contains, e.g. FILTER~"A"
 #define TOK_NOT_IN  39      // does not contain, e.g. FILTER!~"A"
 #define TOK_MODULO  40      // %
+#define TOK_EXT     41      // external values set before each filter_test_ext() call, can be one of {},{str},{int},{float}
 
-//                      0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
+//                      0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
 //                        ( ) [ < = > ] ! | &  +  -  *  /  M  m  a  A  O  ~  ^  S  .  l  f  c  p  b  P  i  s                          %
-static int op_prec[] = {0,1,1,5,5,5,5,5,5,2,3, 6, 6, 7, 7, 8, 8, 8, 3, 2, 5, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7 };
+static int op_prec[] = {0,1,1,5,5,5,5,5,5,2,3, 6, 6, 7, 7, 8, 8, 8, 3, 2, 5, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 0};
 #define TOKEN_STRING "x()[<=>]!|&+-*/MmaAO~^S.lfcpis"       // this is only for debugging, not maintained diligently
 
 static void cmp_vector_strings(token_t *atok, token_t *btok, token_t *rtok);
+inline static void tok_init_samples(token_t *atok, token_t *btok, token_t *rtok);
+
 
 // Return negative values if it is a function with variable number of arguments
 static int filters_next_token(char **str, int *len)
@@ -221,6 +234,10 @@ static int filters_next_token(char **str, int *len)
     if ( !strncasecmp(tmp,"N_PASS(",7) ) { *len = 6; (*str) += 6; return -TOK_FUNC; }
     if ( !strncasecmp(tmp,"F_PASS(",7) ) { *len = 6; (*str) += 6; return -TOK_FUNC; }
     if ( !strncasecmp(tmp,"%ILEN",5) ) { *len = 5; return TOK_VAL; }    // to be able to distinguish between INFO/ILEN and on-the-fly ILEN
+    if ( !strncasecmp(tmp,"{}",2) ) { *len = 2; return TOK_EXT; }
+    if ( !strncasecmp(tmp,"{STR}",5) ) { *len = 5; return TOK_EXT; }
+    if ( !strncasecmp(tmp,"{INT}",5) ) { *len = 5; return TOK_EXT; }
+    if ( !strncasecmp(tmp,"{FLOAT}",7) ) { *len = 7; return TOK_EXT; }
 
     if ( tmp[0]=='@' )  // file name
     {
@@ -596,40 +613,146 @@ static void filters_cmp_filter(token_t *atok, token_t *btok, token_t *rtok, bcf1
         error("Only ==, !=, ~, and !~ operators are supported for FILTER\n");
     return;
 }
-static void filters_cmp_id(token_t *atok, token_t *btok, token_t *rtok, bcf1_t *line)
+static void filters_cmp_string_hash(token_t *atok, token_t *btok, token_t *rtok, bcf1_t *line)
 {
     if ( btok->hash )
     {
         token_t *tmp = atok; atok = btok; btok = tmp;
     }
-    if ( atok->hash )
+    if ( rtok->tok_type!=TOK_EQ && rtok->tok_type!=TOK_NE )
+        error("Only == and != operators are supported for strings read from a file\n");
+
+    // INFO
+    if ( !btok->nsamples )
     {
-        if ( rtok->tok_type!=TOK_EQ && rtok->tok_type!=TOK_NE )
-            error("Only == and != operators are supported for strings read from a file\n");
+        // there is only one string value, e.g. STR[1]=@list.txt
+        if ( btok->idx >= 0 )
+        {
+            int ret = btok->str_value.s ? khash_str2int_has_key(atok->hash, btok->str_value.s) : 0;
+            if ( rtok->tok_type==TOK_NE ) ret = ret ? 0 : 1;
+            rtok->pass_site = ret;
+            return;
+        }
 
-        int ret = khash_str2int_has_key(atok->hash, line->d.id);
+        // there can be multiple comma-separated string values, e.g. STR=@list.txt or STR[*]=@list.txt
+        int ret = 0;
+        char *ptr = btok->str_value.s;
+        while ( *ptr )
+        {
+            char *eptr = ptr + 1;
+            while ( *eptr && *eptr!=',' ) eptr++;
+            char keep = *eptr;
+            *eptr = 0;
+            ret |= khash_str2int_has_key(atok->hash, ptr);
+            *eptr = keep;
+            if ( !keep ) break;
+            ptr = eptr + 1;
+        }
         if ( rtok->tok_type==TOK_NE ) ret = ret ? 0 : 1;
         rtok->pass_site = ret;
         return;
     }
 
-    if ( !btok->str_value.l ) error("Error occurred while evaluating the expression\n");
 
-    if ( rtok->tok_type==TOK_EQ )
-        rtok->pass_site = strcmp(btok->str_value.s,line->d.id) ? 0 : 1;
-    else if ( rtok->tok_type==TOK_NE )
-        rtok->pass_site = strcmp(btok->str_value.s,line->d.id) ? 1 : 0;
-    else
+    // FORMAT
+    tok_init_samples(atok, btok, rtok);
+    rtok->pass_site = 0;
+    int i;
+
+    // there is only one string value, e.g. FMT/STR[*:1]=@list.txt
+    if ( btok->idx >= 0 )
+    {
+        for (i=0; i<btok->nsamples; i++)
+        {
+            if ( !rtok->usmpl[i] ) continue;
+            char *str = btok->str_value.s + i*btok->nval1;
+            char keep = str[btok->nval1];
+            str[btok->nval1] = 0;
+            int ret = khash_str2int_has_key(atok->hash, str);
+            str[btok->nval1] = keep;
+            if ( rtok->tok_type==TOK_NE ) ret = ret ? 0 : 1;
+            rtok->pass_samples[i] = ret;
+            rtok->pass_site |= ret;
+        }
+        return;
+    }
+
+    // there can be multiple comma-separated string values, e.g. FMT/STR=@list.txt
+    for (i=0; i<btok->nsamples; i++)
+    {
+        if ( !rtok->usmpl[i] ) continue;
+        char *str = btok->str_value.s + i*btok->nval1;
+        char keep = str[btok->nval1];
+        str[btok->nval1] = 0;
+
+        // now str contains the block of per-sample comma-separated strings to loop over
+        int ret = 0;
+        char *ptr = str;
+        while ( *ptr )
+        {
+            char *eptr = ptr + 1;
+            while ( *eptr && *eptr!=',' ) eptr++;
+            char keep0 = *eptr;
+            *eptr = 0;
+            ret |= khash_str2int_has_key(atok->hash, ptr);
+            *eptr = keep0;
+            if ( !keep0 ) break;
+            ptr = eptr + 1;
+        }
+        str[btok->nval1] = keep;
+        if ( rtok->tok_type==TOK_NE ) ret = ret ? 0 : 1;
+        rtok->pass_samples[i] = ret;
+        rtok->pass_site |= ret;
+    }
+}
+static void filters_cmp_id(token_t *atok, token_t *btok, token_t *rtok, bcf1_t *line)
+{
+    if ( btok->hash )
     {
-        if ( rtok->tok_type!=TOK_LIKE && rtok->tok_type!=TOK_NLIKE )
-            error("Only the following operators are supported for querying ID: ==, !=, ~, !~; the operator type %d is not supported (%p %p)\n",
-                rtok->tok_type,atok->regex,btok->regex);
+        token_t *tmp = atok; atok = btok; btok = tmp;
+    }
+
+    char *id = line->d.id;
+    int pass = 0;
+
+    while ( id )
+    {
+        char *ep = strchr(id,';');
+        if ( ep ) *ep = 0;
+
+        if ( atok->hash )
+        {
+            if ( rtok->tok_type!=TOK_EQ && rtok->tok_type!=TOK_NE )
+                error("Only == and != operators are supported for strings read from a file\n");
 
-        regex_t *regex = atok->regex ? atok->regex : (btok->regex ? btok->regex : NULL);
-        if ( !regex ) error("fixme: regex initialization failed\n");
-        rtok->pass_site = regexec(regex,line->d.id, 0,NULL,0) ? 0 : 1;
-        if ( rtok->tok_type==TOK_NLIKE ) rtok->pass_site = rtok->pass_site ? 0 : 1;
+            pass = khash_str2int_has_key(atok->hash, id);
+        }
+        else
+        {
+            if ( !btok->str_value.l ) error("Error occurred while evaluating the expression\n");
+
+            if ( rtok->tok_type==TOK_EQ || rtok->tok_type==TOK_NE )
+                pass = strcmp(btok->str_value.s,id) ? 0 : 1;
+            else
+            {
+                if ( rtok->tok_type!=TOK_LIKE && rtok->tok_type!=TOK_NLIKE )
+                    error("Only the following operators are supported for querying ID: ==, !=, ~, !~; the operator type %d is not supported (%p %p)\n",
+                            rtok->tok_type,atok->regex,btok->regex);
+
+                regex_t *regex = atok->regex ? atok->regex : (btok->regex ? btok->regex : NULL);
+                if ( !regex ) error("fixme: regex initialization failed\n");
+                pass = regexec(regex,id, 0,NULL,0) ? 0 : 1;
+            }
+        }
+        if ( ep )
+        {
+            *ep = ';';
+            id = ep + 1;
+        }
+        if ( pass || !ep ) break;
     }
+    if ( rtok->tok_type==TOK_NE || rtok->tok_type==TOK_NE) pass = pass ? 0 : 1;
+    rtok->pass_site = pass;
 }
 
 /**
@@ -637,7 +760,7 @@ static void filters_cmp_id(token_t *atok, token_t *btok, token_t *rtok, bcf1_t *
  *  @line:      BCF line
  *  @info_id:   tag ID, as returned by bcf_hdr_id2int
  *  @ivec:      0-based index to retrieve, -1 when single value is expected
- *  @vptr:      pointer to memory location of sufficient size to accomodate
+ *  @vptr:      pointer to memory location of sufficient size to accommodate
  *              info_id's type
  *
  *  The returned value is -1 if tag is not present, 0 if present but
@@ -659,22 +782,25 @@ static int bcf_get_info_value(bcf1_t *line, int info_id, int ivec, void *value)
     }
 
     if ( ivec<0 ) ivec = 0;
+    if ( ivec>=info->len) return 0;
 
-    #define BRANCH(type_t, is_missing, is_vector_end, out_type_t) { \
-        type_t *p = (type_t *) info->vptr; \
-        for (j=0; j<ivec && j<info->len; j++) \
+    #define BRANCH(type_t, convert, is_missing, is_vector_end, out_type_t) { \
+        uint8_t *p = info->vptr; \
+        for (j=0; j<ivec; j++) \
         { \
+            type_t val = convert(&p[j * sizeof(type_t)]); \
             if ( is_vector_end ) return 0; \
         } \
+        type_t val = convert(&p[ivec * sizeof(type_t)]); \
         if ( is_missing ) return 0; \
-        *((out_type_t*)value) = p[j]; \
+        *((out_type_t*)value) = val; \
         return 1; \
     }
     switch (info->type) {
-        case BCF_BT_INT8:  BRANCH(int8_t,  p[j]==bcf_int8_missing,  p[j]==bcf_int8_vector_end,  int64_t); break;
-        case BCF_BT_INT16: BRANCH(int16_t, p[j]==bcf_int16_missing, p[j]==bcf_int16_vector_end, int64_t); break;
-        case BCF_BT_INT32: BRANCH(int32_t, p[j]==bcf_int32_missing, p[j]==bcf_int32_vector_end, int64_t); break;
-        case BCF_BT_FLOAT: BRANCH(float,   bcf_float_is_missing(p[j]), bcf_float_is_vector_end(p[j]), double); break;
+        case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  int64_t); break;
+        case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, int64_t); break;
+        case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, int64_t); break;
+        case BCF_BT_FLOAT: BRANCH(float,   le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), double); break;
         default: fprintf(stderr,"todo: type %d\n", info->type); exit(1); break;
     }
     #undef BRANCH
@@ -875,6 +1001,7 @@ static void filters_set_format_int(filter_t *flt, bcf1_t *line, token_t *tok)
                 if ( !(flt->cached_GT.mask[i] & (1<<k)) ) continue;
                 dst[j++] = src[k];
             }
+            if ( !j ) { bcf_double_set_missing(dst[j]); j++; }
             for (; j<tok->nval1; j++) bcf_double_set_vector_end(dst[j]);
         }
     }
@@ -965,6 +1092,7 @@ static void filters_set_format_float(filter_t *flt, bcf1_t *line, token_t *tok)
                     dst[j] = src[k];
                 j++;
             }
+            if ( !j ) { bcf_double_set_missing(dst[j]); j++; }
             for (; j<tok->nval1; j++) bcf_double_set_vector_end(dst[j]);
         }
     }
@@ -1008,35 +1136,45 @@ static void filters_set_format_string(filter_t *flt, bcf1_t *line, token_t *tok)
 
     int i, ndim = tok->str_value.m;
     int nstr = bcf_get_format_char(flt->hdr, line, tok->tag, &tok->str_value.s, &ndim);
-    tok->str_value.m = ndim;
+    tok->str_value.m = tok->str_value.l = ndim;
+    kputc(0,&tok->str_value); // append the nul byte
     tok->str_value.l = tok->nvalues = 0;
 
     if ( nstr<0 ) return;
 
+    if ( tok->idx==-3 && filters_cache_genotypes(flt,line)!=0 ) return;
+
     tok->nvalues = tok->str_value.l = nstr;
     tok->nval1   = nstr / tok->nsamples;
     for (i=0; i<tok->nsamples; i++)
     {
         if ( !tok->usmpl[i] ) continue;
         char *src = tok->str_value.s + i*tok->nval1, *dst = src;
-        int ibeg = 0, idx = 0;
+        int ibeg = 0;
+        int idx = 0;        // idx-th field
         while ( ibeg < tok->nval1 )
         {
             int iend = ibeg;
             while ( iend < tok->nval1 && src[iend] && src[iend]!=',' ) iend++;
 
             int keep = 0;
-            if ( tok->idx >= 0 )
+            if ( tok->idx >= 0 )    // single index, given explicitly, e.g. AD[:1]
             {
                 if ( tok->idx==idx ) keep = 1;
             }
-            else if ( idx < tok->nidxs )
+            else if ( tok->idx == -3 )  // given by GT index, e.g. AD[:GT]
             {
-                if ( tok->idxs[idx] != 0 ) keep = 1;
+                if ( flt->cached_GT.mask[i] & (1<<idx) ) keep = 1;
+            }
+            else    // given as a list, e.g. AD[:0,3]
+            {
+                if ( idx < tok->nidxs )
+                {
+                    if ( tok->idxs[idx] != 0 ) keep = 1;
+                }
+                else if ( tok->idxs[tok->nidxs-1] < 0 )
+                    keep = 1;
             }
-            else if ( tok->idxs[tok->nidxs-1] < 0 )
-                keep = 1;
-
             if ( keep )
             {
                 if ( ibeg!=0 ) memmove(dst, src+ibeg, iend-ibeg+1);
@@ -1067,23 +1205,23 @@ static void _filters_set_genotype(filter_t *flt, bcf1_t *line, token_t *tok, int
         tok->str_value.s = (char*)realloc(tok->str_value.s, tok->str_value.m);
     }
 
-#define BRANCH_INT(type_t,vector_end) \
+#define BRANCH_INT(type_t, convert, vector_end) \
     { \
         for (i=0; i<line->n_sample; i++) \
         { \
-            type_t *ptr = (type_t*) (fmt->p + i*fmt->size); \
-            int is_het = 0, has_ref = 0, missing = 0; \
+            uint8_t *ptr = fmt->p + i*fmt->size; \
+            int is_het = 0, has_ref = 0, missing = 0, jal = 0; \
             for (j=0; j<fmt->n; j++) \
             { \
-                if ( ptr[j]==vector_end ) break; /* smaller ploidy */ \
-                if ( bcf_gt_is_missing(ptr[j]) ) { missing=1; break; } /* missing allele */ \
-                int ial = ptr[j]; \
+                type_t ial = convert(&ptr[j * sizeof(type_t)]); \
+                if ( ial==vector_end ) break; /* smaller ploidy */ \
+                if ( bcf_gt_is_missing(ial) ) { missing=1; break; } /* missing allele */ \
                 if ( bcf_gt_allele(ial)==0 ) has_ref = 1; \
                 if ( j>0 ) \
                 { \
-                    int jal = ptr[j-1]; \
                     if ( bcf_gt_allele(ial)!=bcf_gt_allele(jal) ) is_het = 1; \
                 } \
+                jal = ial; \
             } \
             char *dst = &tok->str_value.s[nvals1*i]; \
             if ( type==4 ) \
@@ -1121,9 +1259,9 @@ static void _filters_set_genotype(filter_t *flt, bcf1_t *line, token_t *tok, int
         } \
     }
     switch (fmt->type) {
-        case BCF_BT_INT8:  BRANCH_INT(int8_t,  bcf_int8_vector_end); break;
-        case BCF_BT_INT16: BRANCH_INT(int16_t, bcf_int16_vector_end); break;
-        case BCF_BT_INT32: BRANCH_INT(int32_t, bcf_int32_vector_end); break;
+        case BCF_BT_INT8:  BRANCH_INT(int8_t,  le_to_i8,  bcf_int8_vector_end); break;
+        case BCF_BT_INT16: BRANCH_INT(int16_t, le_to_i16, bcf_int16_vector_end); break;
+        case BCF_BT_INT32: BRANCH_INT(int32_t, le_to_i32, bcf_int32_vector_end); break;
         default: error("The GT type is not lineognised: %d at %s:%"PRId64"\n",fmt->type, bcf_seqname(flt->hdr,line),(int64_t) line->pos+1); break;
     }
 #undef BRANCH_INT
@@ -1180,9 +1318,13 @@ static void filters_set_ilen(filter_t *flt, bcf1_t *line, token_t *tok)
     int i, rlen = strlen(line->d.allele[0]);
     for (i=1; i<line->n_allele; i++)
     {
+        if ( line->d.allele[i][0]=='<' )
+        {
+            bcf_double_set_missing(tok->values[i-1]);
+            continue;
+        }
         int alen = strlen(line->d.allele[i]);
-        if ( rlen==alen ) bcf_double_set_missing(tok->values[i-1]);
-        else tok->values[i-1] = alen - rlen;
+        tok->values[i-1] = alen - rlen;
     }
 }
 static void filters_set_ref_string(filter_t *flt, bcf1_t *line, token_t *tok)
@@ -1247,21 +1389,22 @@ static void filters_set_nmissing(filter_t *flt, bcf1_t *line, token_t *tok)
     }
 
     int j,nmissing = 0;
-    #define BRANCH(type_t, is_vector_end) { \
+    #define BRANCH(type_t, convert, is_vector_end) { \
         for (i=0; i<line->n_sample; i++) \
         { \
-            type_t *ptr = (type_t *) (fmt->p + i*fmt->size); \
+            uint8_t *ptr = fmt->p + i*fmt->size; \
             for (j=0; j<fmt->n; j++) \
             { \
-                if ( ptr[j]==is_vector_end ) break; \
-                if ( ptr[j]==bcf_gt_missing ) { nmissing++; break; } \
+                type_t val = convert(&ptr[j * sizeof(type_t)]); \
+                if ( val==is_vector_end ) break; \
+                if ( val==bcf_gt_missing ) { nmissing++; break; } \
             } \
         } \
     }
     switch (fmt->type) {
-        case BCF_BT_INT8:  BRANCH(int8_t,  bcf_int8_vector_end); break;
-        case BCF_BT_INT16: BRANCH(int16_t, bcf_int16_vector_end); break;
-        case BCF_BT_INT32: BRANCH(int32_t, bcf_int32_vector_end); break;
+        case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  bcf_int8_vector_end); break;
+        case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, bcf_int16_vector_end); break;
+        case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, bcf_int32_vector_end); break;
         default: fprintf(stderr,"todo: type %d\n", fmt->type); exit(1); break;
     }
     #undef BRANCH
@@ -1893,19 +2036,6 @@ static int func_strlen(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **sta
     }
     return 1;
 }
-static inline double calc_binom(int na, int nb)
-{
-    if ( na==0 && nb==0 ) return -1;
-    if ( na==nb ) return 1;
-
-    // kfunc.h implements kf_betai, which is the regularized beta function  P(X<=k/N;p) = I_{1-p}(N-k,k+1)
-
-    double pval = na < nb ? kf_betai(nb, na + 1, 0.5) : kf_betai(na, nb + 1, 0.5);
-    pval *= 2;
-    if ( pval>1 ) pval = 1;     // this can happen, machine precision error, eg. kf_betai(1,0,0.5)
-
-    return pval;
-}
 static int func_binom(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **stack, int nstack)
 {
     int i, istack = nstack - rtok->nargs;
@@ -1973,7 +2103,7 @@ static int func_binom(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **stac
                     bcf_double_set_missing(rtok->values[i]);
                     continue;
                 }
-                rtok->values[i] = calc_binom(vals[idx1],vals[idx2]);
+                rtok->values[i] = calc_binom_two_sided(vals[idx1],vals[idx2],0.5);
                 if ( rtok->values[i] < 0 )
                 {
                     bcf_double_set_missing(rtok->values[i]);
@@ -1997,7 +2127,7 @@ static int func_binom(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **stac
                     bcf_double_set_missing(rtok->values[i]);
                     continue;
                 }
-                rtok->values[i] = calc_binom(ptr1[0],ptr2[0]);
+                rtok->values[i] = calc_binom_two_sided(ptr1[0],ptr2[0],0.5);
                 if ( rtok->values[i] < 0 )
                 {
                     bcf_double_set_missing(rtok->values[i]);
@@ -2036,7 +2166,7 @@ static int func_binom(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **stac
             bcf_double_set_missing(rtok->values[0]);
         else
         {
-            rtok->values[0] = calc_binom(ptr1[0],ptr2[0]);
+            rtok->values[0] = calc_binom_two_sided(ptr1[0],ptr2[0],0.5);
             if ( rtok->values[0] < 0 )
                 bcf_double_set_missing(rtok->values[0]);
         }
@@ -2306,6 +2436,18 @@ static int vector_logic_and(filter_t *filter, bcf1_t *line, token_t *rtok, token
     return 2;
 }
 
+// A note about comparisons:
+// When setting value by determining index from the genotype, we face the problem
+// of how to interpret truncating arrays. Say we have TAG defined as Number=. and
+//      GT:TAG   1/1:0,1,2  0/0:0
+// Then when querying we expect the following expression to evaluate for the second
+// sample as
+//      -i 'TAG[1:1]="."'  .. true
+//      -i 'TAG[1:GT]="."' .. false
+// The problem is that the implementation truncates the number of fields, filling
+// usually fewer than the original number of per-sample values. This is fixed by
+// adding an exception that makes the code aware of this: the GT indexing can be
+// recognised by having tok->idx==-3
 #define CMP_VECTORS(atok,btok,_rtok,CMP_OP,missing_logic) \
 { \
     token_t *rtok = _rtok; \
@@ -2393,6 +2535,8 @@ static int vector_logic_and(filter_t *filter, bcf1_t *line, token_t *rtok, token
                 double *bptr = btok->values + i*btok->nval1; \
                 for (j=0; j<atok->nval1; j++) \
                 { \
+                    if ( atok->idx==-3 && bcf_double_is_vector_end(aptr[j]) ) break; /* explained above */ \
+                    if ( btok->idx==-3 && bcf_double_is_vector_end(bptr[j]) ) break; /* explained above */ \
                     int nmiss = bcf_double_is_missing_or_vector_end(aptr[j]) ? 1 : 0; \
                     if ( nmiss && !missing_logic[0] ) continue; /* any is missing => result is false */ \
                     nmiss += (bcf_double_is_missing_or_vector_end(bptr[j]) ? 1 : 0); \
@@ -2414,9 +2558,10 @@ static int vector_logic_and(filter_t *filter, bcf1_t *line, token_t *rtok, token
             { \
                 if ( !rtok->usmpl[i] ) continue; \
                 double *aptr = atok->values + i*atok->nval1; \
-                double *bptr = btok->values + i*btok->nval1; \
+                double *bptr = btok->values; \
                 for (j=0; j<atok->nval1; j++) \
                 { \
+                    if ( atok->idx==-3 && bcf_double_is_vector_end(aptr[j]) ) break; /* explained above */ \
                     int miss = bcf_double_is_missing_or_vector_end(aptr[j]) ? 1 : 0; \
                     if ( miss && !missing_logic[0] ) continue; /* any is missing => result is false */ \
                     for (k=0; k<btok->nvalues; k++) \
@@ -2440,10 +2585,11 @@ static int vector_logic_and(filter_t *filter, bcf1_t *line, token_t *rtok, token
             for (i=0; i<btok->nsamples; i++) \
             { \
                 if ( !rtok->usmpl[i] ) continue; \
-                double *aptr = atok->values + i*atok->nval1; \
+                double *aptr = atok->values; \
                 double *bptr = btok->values + i*btok->nval1; \
                 for (j=0; j<btok->nval1; j++) \
                 { \
+                    if ( atok->idx==-3 && bcf_double_is_vector_end(bptr[j]) ) break; /* explained above */ \
                     int miss = bcf_double_is_missing_or_vector_end(bptr[j]) ? 1 : 0; \
                     if ( miss && !missing_logic[0] ) continue; /* any is missing => result is false */ \
                     for (k=0; k<atok->nvalues; k++) \
@@ -2807,9 +2953,26 @@ static int max_ac_an_unpack(bcf_hdr_t *hdr)
 
     return BCF_UN_INFO;
 }
+static int filters_init1_ext(filter_t *filter, char *str, int len, token_t *tok)
+{
+    tok->hl_type  = -1;
+    tok->ht_type  = -1;
+    tok->tok_type  = TOK_VAL;
+    tok->hdr_id    = -1;
+    tok->pass_site = -1;
+    tok->idx       = 0;
+    tok->iext = ++filter->n_ext;
+    filter->ext = realloc(filter->ext,sizeof(*filter->ext)*filter->n_ext);
+    if ( !strncasecmp(str,"{str}",len) ) { tok->ht_type = BCF_HT_STR; tok->is_str = 1; }
+    else if ( !strncasecmp(str,"{int}",len) ) tok->ht_type = BCF_HT_INT;
+    else if ( !strncasecmp(str,"{float}",len) ) tok->ht_type = BCF_HT_REAL;
+    filter->ext[filter->n_ext-1] = tok->ht_type;
+    return 0;
+}
 static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
 {
-    tok->tag_type  = -1;
+    tok->ht_type  = -1;
+    tok->hl_type  = -1;
     tok->tok_type  = TOK_VAL;
     tok->hdr_id    = -1;
     tok->pass_site = -1;
@@ -2826,6 +2989,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         memcpy(tok->key,str+1,len-2);
         tok->key[len-2] = 0;
         tok->is_str = 1;
+        tok->ht_type = BCF_HT_STR;
         tok->nvalues = len-2;
         if ( !strcmp(".",tok->key) ) tok->is_missing = 1;
         return 0;
@@ -2867,6 +3031,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = filters_set_qual;
             tok->tag = strdup("QUAL");
+            tok->ht_type = BCF_HT_REAL;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2874,6 +3039,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = filters_set_type;
             tok->tag = strdup("TYPE");
+            tok->ht_type = BCF_HT_STR;
             return 0;
         }
         else if ( !strncasecmp(str,"FILTER",len) || !strncmp(str,"%FILTER",len) /* for backward compatibility */ )
@@ -2881,7 +3047,8 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             tok->comparator = filters_cmp_filter;
             tok->tag = strdup("FILTER");
             filter->max_unpack |= BCF_UN_FLT;
-            tok->tag_type = BCF_HL_FLT;
+            tok->hl_type = BCF_HL_FLT;
+            tok->ht_type = BCF_HT_STR;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2889,6 +3056,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->comparator = filters_cmp_id;
             tok->tag = strdup("ID");
+            tok->ht_type = BCF_HT_STR;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2896,6 +3064,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = &filters_set_chrom;
             tok->tag = strdup("CHROM");
+            tok->ht_type = BCF_HT_STR;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2903,6 +3072,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = &filters_set_pos;
             tok->tag = strdup("POS");
+            tok->ht_type = BCF_HT_INT;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2911,6 +3081,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             tok->setter = &filters_set_ref_string;
             tok->is_str = 1;
             tok->tag = strdup("REF");
+            tok->ht_type = BCF_HT_STR;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2919,6 +3090,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             tok->setter = &filters_set_alt_string;
             tok->is_str = 1;
             tok->tag = strdup("ALT");
+            tok->ht_type = BCF_HT_STR;
             tok->idxs = (int*) malloc(sizeof(int));
             tok->idxs[0] = -1;
             tok->nidxs   = 1;
@@ -2930,6 +3102,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = &filters_set_nalt;
             tok->tag = strdup("N_ALT");
+            tok->ht_type = BCF_HT_INT;
             return 0;
         }
         else if ( !strncasecmp(str,"N_SAMPLES",len) )
@@ -2937,6 +3110,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             tok->tok_type = TOK_VAL;
             tok->threshold = bcf_hdr_nsamples(filter->hdr);
             tok->is_constant = 1;
+            tok->ht_type = BCF_HT_INT;
             return 0;
         }
         else if ( !strncasecmp(str,"N_MISSING",len) )
@@ -2944,6 +3118,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             filter->max_unpack |= BCF_UN_FMT;
             tok->setter = &filters_set_nmissing;
             tok->tag = strdup("N_MISSING");
+            tok->ht_type = BCF_HT_INT;
             return 0;
         }
         else if ( !strncasecmp(str,"F_MISSING",len) )
@@ -2951,6 +3126,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             filter->max_unpack |= BCF_UN_FMT;
             tok->setter = &filters_set_nmissing;
             tok->tag = strdup("F_MISSING");
+            tok->ht_type = BCF_HT_REAL;
             return 0;
         }
     }
@@ -2991,13 +3167,14 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         for (i=0; i<tok->nsamples; i++) tok->usmpl[i] = 1;
     }
 
-    tok->tag_type = is_fmt ? BCF_HL_FMT : BCF_HL_INFO;
+    tok->hl_type = is_fmt ? BCF_HL_FMT : BCF_HL_INFO;
     if ( is_fmt ) filter->max_unpack |= BCF_UN_FMT;
     if ( tok->hdr_id>=0 )
     {
         if ( is_fmt && !strcmp("GT",tmp.s) )
         {
             tok->setter = &filters_set_genotype_string; tok->is_str = 1;
+            tok->ht_type = BCF_HT_STR;
         }
         else if ( is_fmt )
         {
@@ -3012,9 +3189,9 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             }
             switch ( bcf_hdr_id2type(filter->hdr,BCF_HL_FMT,tok->hdr_id) )
             {
-                case BCF_HT_INT:  tok->setter = &filters_set_format_int; break;
-                case BCF_HT_REAL: tok->setter = &filters_set_format_float; break;
-                case BCF_HT_STR:  tok->setter = &filters_set_format_string; tok->is_str = 1; break;
+                case BCF_HT_INT:  tok->setter = &filters_set_format_int; tok->ht_type = BCF_HT_INT; break;
+                case BCF_HT_REAL: tok->setter = &filters_set_format_float; tok->ht_type = BCF_HT_REAL; break;
+                case BCF_HT_STR:  tok->setter = &filters_set_format_string; tok->ht_type = BCF_HT_STR; tok->is_str = 1; break;
                 default: error("[%s:%d %s] FIXME\n", __FILE__,__LINE__,__FUNCTION__);
             }
         }
@@ -3023,10 +3200,14 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         else
         {
             if ( bcf_hdr_id2type(filter->hdr,BCF_HL_INFO,tok->hdr_id) == BCF_HT_FLAG )
+            {
                 tok->setter = filters_set_info_flag;
+                tok->ht_type = BCF_HT_INT;
+            }
             else
             {
-                if ( bcf_hdr_id2type(filter->hdr,BCF_HL_INFO,tok->hdr_id) == BCF_HT_STR ) tok->is_str = 1;
+                tok->ht_type = bcf_hdr_id2type(filter->hdr,BCF_HL_INFO,tok->hdr_id);
+                if ( tok->ht_type == BCF_HT_STR ) tok->is_str = 1;
                 if ( bcf_hdr_id2number(filter->hdr,BCF_HL_INFO,tok->hdr_id)==1 )
                     tok->setter = filters_set_info;
                 else
@@ -3058,6 +3239,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
     {
         tok->setter = &filters_set_alt_string;
         tok->is_str = 1;
+        tok->ht_type = BCF_HT_STR;
         tok->tag = strdup(tmp.s);
         free(tmp.s);
         filter_add_used_tag(filter,NULL,tok->tag);
@@ -3068,6 +3250,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= BCF_UN_FMT;
         tok->setter = &filters_set_an;
         tok->tag = strdup("AN");
+        tok->ht_type = BCF_HT_INT;
         free(tmp.s);
         return 0;
     }
@@ -3076,6 +3259,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= BCF_UN_FMT;
         tok->setter = &filters_set_ac;
         tok->tag = strdup("AC");
+        tok->ht_type = BCF_HT_INT;
         free(tmp.s);
         return 0;
     }
@@ -3084,6 +3268,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= max_ac_an_unpack(filter->hdr);
         tok->setter = &filters_set_mac;
         tok->tag = strdup("MAC");
+        tok->ht_type = BCF_HT_INT;
         free(tmp.s);
         return 0;
     }
@@ -3092,6 +3277,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= max_ac_an_unpack(filter->hdr);
         tok->setter = &filters_set_af;
         tok->tag = strdup("AF");
+        tok->ht_type = BCF_HT_REAL;
         free(tmp.s);
         return 0;
     }
@@ -3100,6 +3286,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= max_ac_an_unpack(filter->hdr);
         tok->setter = &filters_set_maf;
         tok->tag = strdup("MAF");
+        tok->ht_type = BCF_HT_REAL;
         free(tmp.s);
         return 0;
     }
@@ -3108,6 +3295,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= BCF_UN_STR;
         tok->setter = &filters_set_ilen;
         tok->tag = strdup("ILEN");
+        tok->ht_type = BCF_HT_INT;
         free(tmp.s);
         return 0;
     }
@@ -3130,7 +3318,10 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             filter->status |= FILTER_ERR_UNKN_TAGS;
             filter_add_undef_tag(filter,tmp.s);
         }
+        tok->ht_type = BCF_HT_REAL;
     }
+    else
+        tok->ht_type = BCF_HT_INT;
     tok->is_constant = 1;
 
     if ( tmp.s ) free(tmp.s);
@@ -3284,6 +3475,33 @@ static void perl_destroy(filter_t *filter)
 #endif
 }
 
+// A very rudimentary heuristics to determine type, e.g. STR_TAG={} implies {str}.
+// Throws an error on anything more complex and asks for an explicit type.
+static void determine_ext_types(filter_t *filter, int ntok, token_t *tok)
+{
+    int i;
+    for (i=0; i<ntok; i++)
+    {
+        if ( !tok[i].iext || tok[i].ht_type!=-1 ) continue;
+        if ( !i || i+1==ntok ) break;       // first or last in the RPN
+        if ( tok[i-1].ht_type==-1 ) break;  // previous type not set
+        // todo: check if the next is an operator
+        // todo: case when the order is reversed, {}=TAG
+        tok[i].ht_type = tok[i-1].ht_type;
+    }
+    if ( i!=ntok )
+        error("[%s:%d %s] Error: unable to determine the type, use explicit notation: %s\n",__FILE__,__LINE__,__FUNCTION__,filter->str);
+    for (i=0; i<ntok; i++)
+    {
+        int j = tok[i].iext - 1;
+        if ( j<0 ) continue;
+        if ( filter->ext[j]!=-1 && filter->ext[j]!=tok[i].ht_type  )
+            error("[%s:%d %s] FIXME: this should not happen %d vs %d, iext=%d\n",__FILE__,__LINE__,__FUNCTION__,filter->ext[j],tok[i].ht_type,j);
+        filter->ext[j] = tok[i].ht_type;
+        if ( tok[i].ht_type==BCF_HT_STR ) tok[i].is_str = 1;
+    }
+}
+
 
 // Parse filter expression and convert to reverse polish notation. Dijkstra's shunting-yard algorithm
 static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error)
@@ -3331,6 +3549,13 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
             memset(&ops[nops-1],0,sizeof(token_t));
             nops--;
         }
+        else if ( ret==TOK_EXT )    // external value
+        {
+            nout++;
+            hts_expand0(token_t, nout, mout, out);
+            filters_init1_ext(filter, tmp, len, &out[nout-1]);
+            tmp += len;
+        }
         else if ( ret!=TOK_VAL )    // one of the operators
         {
             // detect unary minus: replace -value with -1*(value)
@@ -3441,12 +3666,12 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
             hts_expand0(token_t, nops, mops, ops);
             ops[nops-1].tok_type = ret;
         }
-        else if ( !len )
+        else if ( !len )    // all tokes read or an error
         {
             if ( *tmp && !isspace(*tmp) ) error("Could not parse the expression: [%s]\n", str);
             break;     // all tokens read
         }
-        else           // annotation name or filtering value
+        else           // TOK_VAL: annotation name or value
         {
             nout++;
             hts_expand0(token_t, nout, mout, out);
@@ -3468,10 +3693,21 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
         nops--;
     }
 
+    if ( filter->status != FILTER_OK )
+    {
+        if ( mops ) free(ops);
+        filter->filters   = out;
+        filter->nfilters  = nout;
+        return filter;
+    }
+
+    // Determine types of external variables from the context
+    determine_ext_types(filter,nout,out);
+
     // In the special cases of TYPE and FILTER the BCF header IDs are yet unknown. Walk through the
     // list of operators and convert the strings (e.g. "PASS") to BCF ids. The string value token must be
     // just before or after the FILTER token and they must be followed with a comparison operator.
-    // At this point we also initialize regex expressions which, in RPN, must preceed the LIKE/NLIKE operator.
+    // At this point we also initialize regex expressions which, in RPN, must precede the LIKE/NLIKE operator.
     // Additionally, treat "." as missing value rather than a string in numeric equalities; that
     // @file is only used with ID; etc.
     // This code is fragile: improve me.
@@ -3486,7 +3722,10 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
         {
             int j = out[i+1].tok_type==TOK_VAL ? i+1 : i-1;
             if ( out[j].comparator!=filters_cmp_id )
-                error("Error: could not parse the expression. Note that the \"@file_name\" syntax can be currently used with ID column only.\n");
+            {
+                if ( out[j].comparator ) error("Error: could not parse the expression with \"@file_name\" syntax (possible todo)\n");
+                out[j].comparator = filters_cmp_string_hash;
+            }
         }
         if ( out[i].tok_type==TOK_OR || out[i].tok_type==TOK_OR_VEC )
             out[i].func = vector_logic_or;
@@ -3502,7 +3741,7 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
                 int set_missing = 0;
                 if ( out[k].hdr_id>0 )
                 {
-                    int type = bcf_hdr_id2type(filter->hdr,out[k].tag_type,out[k].hdr_id);
+                    int type = bcf_hdr_id2type(filter->hdr,out[k].hl_type,out[k].hdr_id);
                     if ( type==BCF_HT_INT ) set_missing = 1;
                     else if ( type==BCF_HT_REAL ) set_missing = 1;
                 }
@@ -3543,7 +3782,8 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
         if ( !out[i].tag ) continue;
         if ( out[i].setter==filters_set_type )
         {
-            if ( i+1==nout ) error("Could not parse the expression: %s\n", filter->str);
+            if ( i+1==nout || !out[i+1].key )
+                error("Could not parse the expression: %s\n", filter->str);
             int itok, ival;
             if ( out[i+1].tok_type==TOK_EQ || out[i+1].tok_type==TOK_NE ) ival = i - 1, itok = i + 1;
             else if ( out[i+1].tok_type==TOK_LIKE || out[i+1].tok_type==TOK_NLIKE ) ival = i - 1, itok = i + 1;
@@ -3591,7 +3831,7 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
             else if ( !strcasecmp(out[ival].key,"r") ) { out[i].setter = filters_set_genotype2; out[ival].key[0]='r'; out[ival].key[1]=0; }  // r
             continue;
         }
-        if ( out[i].tag_type==BCF_HL_FLT )
+        if ( out[i].hl_type==BCF_HL_FLT )
         {
             if ( i+1==nout ) error("Could not parse the expression: %s\n", filter->str);
             int itok = i, ival;
@@ -3705,6 +3945,7 @@ void filter_destroy(filter_t *filter)
     }
     for (i=0; i<filter->nundef_tag; i++) free(filter->undef_tag[i]);
     for (i=0; i<filter->nused_tag; i++) free(filter->used_tag[i]);
+    free(filter->ext);
     free(filter->undef_tag);
     free(filter->used_tag);
     free(filter->cached_GT.buf);
@@ -3718,6 +3959,37 @@ void filter_destroy(filter_t *filter)
     free(filter);
 }
 
+int filter_test_ext(filter_t *filter, bcf1_t *rec, const uint8_t **samples, const void **ext)
+{
+    if ( !filter->n_ext )
+        return filter_test(filter,rec,samples);
+
+    int i;
+    for (i=0; i<filter->nfilters; i++)
+    {
+        token_t *tok = &filter->filters[i];
+        if ( !tok->iext ) continue;
+        if ( !ext[tok->iext-1] )
+        {
+            tok->is_missing = 1;
+            tok->nvalues = 0;
+            if ( filter->ext[tok->iext-1]==BCF_HT_STR ) tok->str_value.l = 0;
+            continue;
+        }
+        tok->is_missing = 0;
+        tok->nvalues = 1;
+        if ( filter->ext[tok->iext-1]==BCF_HT_STR )
+        {
+            tok->str_value.l = 0;
+            kputs((const char*)ext[tok->iext-1],&tok->str_value);
+            tok->nvalues = tok->str_value.l;
+        }
+        else if ( filter->ext[tok->iext-1]==BCF_HT_INT ) tok->values[0] = *((const int*)ext[tok->iext-1]);
+        else if ( filter->ext[tok->iext-1]==BCF_HT_REAL ) tok->values[0] = *((const float*)ext[tok->iext-1]);
+    }
+    return filter_test(filter,rec,samples);
+}
+
 int filter_test(filter_t *filter, bcf1_t *line, const uint8_t **samples)
 {
     if ( filter->status != FILTER_OK ) error("Error: the caller did not check the filter status\n");
@@ -3854,7 +4126,11 @@ int filter_max_unpack(filter_t *flt)
 {
     return flt->max_unpack;
 }
-
+const int *filter_ext_types(filter_t *filter, int *n_ext)
+{
+    *n_ext = filter->n_ext;
+    return filter->ext;
+}
 const double *filter_get_doubles(filter_t *filter, int *nval, int *nval1)
 {
     token_t *tok = filter->flt_stack[0];
index d0e26258cc4f65554418081bc5e08fdace71f504..2db56801b5c91c3c813bcea9c5c559c416ecc2bb 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  filter.c -- filter expressions.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -31,6 +31,7 @@ THE SOFTWARE.  */
 #include <errno.h>
 #include <math.h>
 #include <sys/types.h>
+#include <stdint.h>
 #include <inttypes.h>
 #ifndef _WIN32
 #include <pwd.h>
@@ -40,11 +41,17 @@ THE SOFTWARE.  */
 #include <htslib/hts_defs.h>
 #include <htslib/vcfutils.h>
 #include <htslib/kfunc.h>
+#include <htslib/hts_endian.h>
 #include "config.h"
 #include "filter.h"
 #include "bcftools.h"
 
 #if ENABLE_PERL_FILTERS
+// Work around clang warning problems
+#  if defined(__clang__)
+#    define PERL_GCC_BRACE_GROUPS_FORBIDDEN
+#  endif
+
 #  define filter_t perl_filter_t
 #  include <EXTERN.h>
 #  include <perl.h>
@@ -68,7 +75,7 @@ typedef struct _token_t
     char *tag;          // for debugging and printout only, VCF tag name
     double threshold;   // filtering threshold
     int is_constant;    // the threshold is set
-    int hdr_id, tag_type;   // BCF header lookup ID and one of BCF_HL_* types
+    int hdr_id, hl_type, ht_type;   // BCF header lookup ID and one of BCF_HL_* types and BCF_HT_* types
     int idx;            // 0-based index to VCF vectors,
                         //  -2: list (e.g. [0,1,2] or [1..3] or [1..] or any field[*], which is equivalent to [0..])
                         //  -3: select indices on the fly based on values in GT
@@ -81,11 +88,12 @@ typedef struct _token_t
     void (*comparator)(struct _token_t *, struct _token_t *, struct _token_t *rtok, bcf1_t *);
     void *hash;         // test presence of str value in the hash via comparator
     regex_t *regex;     // precompiled regex for string comparison
+    int iext;           // for the use with filter_test_ext(), 1-based index to external values, 0=don't use
 
     // modified on filter evaluation at each VCF line
     double *values;
     kstring_t str_value;
-    int is_str, is_missing; // is_missing is set only for constants, variables are controled via nvalues
+    int is_str, is_missing; // is_missing is set only for constants, variables are controlled via nvalues
     int pass_site;          // -1 not applicable, 0 fails, >0 pass
     uint8_t *pass_samples;  // status of individual samples
     int nvalues, mvalues;   // number of used values: n=0 for missing values, n=1 for scalars, for strings n=str_value.l
@@ -114,6 +122,8 @@ struct _filter_t
     char **undef_tag, **used_tag;
     int nundef_tag, nused_tag;
     int status, exit_on_error;
+    int n_ext;      // number of external values to fill via filter_test_ext()
+    int *ext;       // types of external values to fill via filter_test_ext()
 };
 
 
@@ -158,13 +168,16 @@ struct _filter_t
 #define TOK_IN      38      // contains, e.g. FILTER~"A"
 #define TOK_NOT_IN  39      // does not contain, e.g. FILTER!~"A"
 #define TOK_MODULO  40      // %
+#define TOK_EXT     41      // external values set before each filter_test_ext() call, can be one of {},{str},{int},{float}
 
-//                      0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
+//                      0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
 //                        ( ) [ < = > ] ! | &  +  -  *  /  M  m  a  A  O  ~  ^  S  .  l  f  c  p  b  P  i  s                          %
-static int op_prec[] = {0,1,1,5,5,5,5,5,5,2,3, 6, 6, 7, 7, 8, 8, 8, 3, 2, 5, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7 };
+static int op_prec[] = {0,1,1,5,5,5,5,5,5,2,3, 6, 6, 7, 7, 8, 8, 8, 3, 2, 5, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 0};
 #define TOKEN_STRING "x()[<=>]!|&+-*/MmaAO~^S.lfcpis"       // this is only for debugging, not maintained diligently
 
 static void cmp_vector_strings(token_t *atok, token_t *btok, token_t *rtok);
+inline static void tok_init_samples(token_t *atok, token_t *btok, token_t *rtok);
+
 
 // Return negative values if it is a function with variable number of arguments
 static int filters_next_token(char **str, int *len)
@@ -223,6 +236,10 @@ static int filters_next_token(char **str, int *len)
     if ( !strncasecmp(tmp,"N_PASS(",7) ) { *len = 6; (*str) += 6; return -TOK_FUNC; }
     if ( !strncasecmp(tmp,"F_PASS(",7) ) { *len = 6; (*str) += 6; return -TOK_FUNC; }
     if ( !strncasecmp(tmp,"%ILEN",5) ) { *len = 5; return TOK_VAL; }    // to be able to distinguish between INFO/ILEN and on-the-fly ILEN
+    if ( !strncasecmp(tmp,"{}",2) ) { *len = 2; return TOK_EXT; }
+    if ( !strncasecmp(tmp,"{STR}",5) ) { *len = 5; return TOK_EXT; }
+    if ( !strncasecmp(tmp,"{INT}",5) ) { *len = 5; return TOK_EXT; }
+    if ( !strncasecmp(tmp,"{FLOAT}",7) ) { *len = 7; return TOK_EXT; }
 
     if ( tmp[0]=='@' )  // file name
     {
@@ -598,40 +615,146 @@ static void filters_cmp_filter(token_t *atok, token_t *btok, token_t *rtok, bcf1
         error("Only ==, !=, ~, and !~ operators are supported for FILTER\n");
     return;
 }
-static void filters_cmp_id(token_t *atok, token_t *btok, token_t *rtok, bcf1_t *line)
+static void filters_cmp_string_hash(token_t *atok, token_t *btok, token_t *rtok, bcf1_t *line)
 {
     if ( btok->hash )
     {
         token_t *tmp = atok; atok = btok; btok = tmp;
     }
-    if ( atok->hash )
+    if ( rtok->tok_type!=TOK_EQ && rtok->tok_type!=TOK_NE )
+        error("Only == and != operators are supported for strings read from a file\n");
+
+    // INFO
+    if ( !btok->nsamples )
     {
-        if ( rtok->tok_type!=TOK_EQ && rtok->tok_type!=TOK_NE )
-            error("Only == and != operators are supported for strings read from a file\n");
+        // there is only one string value, e.g. STR[1]=@list.txt
+        if ( btok->idx >= 0 )
+        {
+            int ret = btok->str_value.s ? khash_str2int_has_key(atok->hash, btok->str_value.s) : 0;
+            if ( rtok->tok_type==TOK_NE ) ret = ret ? 0 : 1;
+            rtok->pass_site = ret;
+            return;
+        }
 
-        int ret = khash_str2int_has_key(atok->hash, line->d.id);
+        // there can be multiple comma-separated string values, e.g. STR=@list.txt or STR[*]=@list.txt
+        int ret = 0;
+        char *ptr = btok->str_value.s;
+        while ( *ptr )
+        {
+            char *eptr = ptr + 1;
+            while ( *eptr && *eptr!=',' ) eptr++;
+            char keep = *eptr;
+            *eptr = 0;
+            ret |= khash_str2int_has_key(atok->hash, ptr);
+            *eptr = keep;
+            if ( !keep ) break;
+            ptr = eptr + 1;
+        }
         if ( rtok->tok_type==TOK_NE ) ret = ret ? 0 : 1;
         rtok->pass_site = ret;
         return;
     }
 
-    if ( !btok->str_value.l ) error("Error occurred while evaluating the expression\n");
 
-    if ( rtok->tok_type==TOK_EQ )
-        rtok->pass_site = strcmp(btok->str_value.s,line->d.id) ? 0 : 1;
-    else if ( rtok->tok_type==TOK_NE )
-        rtok->pass_site = strcmp(btok->str_value.s,line->d.id) ? 1 : 0;
-    else
+    // FORMAT
+    tok_init_samples(atok, btok, rtok);
+    rtok->pass_site = 0;
+    int i;
+
+    // there is only one string value, e.g. FMT/STR[*:1]=@list.txt
+    if ( btok->idx >= 0 )
+    {
+        for (i=0; i<btok->nsamples; i++)
+        {
+            if ( !rtok->usmpl[i] ) continue;
+            char *str = btok->str_value.s + i*btok->nval1;
+            char keep = str[btok->nval1];
+            str[btok->nval1] = 0;
+            int ret = khash_str2int_has_key(atok->hash, str);
+            str[btok->nval1] = keep;
+            if ( rtok->tok_type==TOK_NE ) ret = ret ? 0 : 1;
+            rtok->pass_samples[i] = ret;
+            rtok->pass_site |= ret;
+        }
+        return;
+    }
+
+    // there can be multiple comma-separated string values, e.g. FMT/STR=@list.txt
+    for (i=0; i<btok->nsamples; i++)
+    {
+        if ( !rtok->usmpl[i] ) continue;
+        char *str = btok->str_value.s + i*btok->nval1;
+        char keep = str[btok->nval1];
+        str[btok->nval1] = 0;
+
+        // now str contains the block of per-sample comma-separated strings to loop over
+        int ret = 0;
+        char *ptr = str;
+        while ( *ptr )
+        {
+            char *eptr = ptr + 1;
+            while ( *eptr && *eptr!=',' ) eptr++;
+            char keep0 = *eptr;
+            *eptr = 0;
+            ret |= khash_str2int_has_key(atok->hash, ptr);
+            *eptr = keep0;
+            if ( !keep0 ) break;
+            ptr = eptr + 1;
+        }
+        str[btok->nval1] = keep;
+        if ( rtok->tok_type==TOK_NE ) ret = ret ? 0 : 1;
+        rtok->pass_samples[i] = ret;
+        rtok->pass_site |= ret;
+    }
+}
+static void filters_cmp_id(token_t *atok, token_t *btok, token_t *rtok, bcf1_t *line)
+{
+    if ( btok->hash )
     {
-        if ( rtok->tok_type!=TOK_LIKE && rtok->tok_type!=TOK_NLIKE )
-            error("Only the following operators are supported for querying ID: ==, !=, ~, !~; the operator type %d is not supported (%p %p)\n",
-                rtok->tok_type,atok->regex,btok->regex);
+        token_t *tmp = atok; atok = btok; btok = tmp;
+    }
+
+    char *id = line->d.id;
+    int pass = 0;
+
+    while ( id )
+    {
+        char *ep = strchr(id,';');
+        if ( ep ) *ep = 0;
+
+        if ( atok->hash )
+        {
+            if ( rtok->tok_type!=TOK_EQ && rtok->tok_type!=TOK_NE )
+                error("Only == and != operators are supported for strings read from a file\n");
 
-        regex_t *regex = atok->regex ? atok->regex : (btok->regex ? btok->regex : NULL);
-        if ( !regex ) error("fixme: regex initialization failed\n");
-        rtok->pass_site = regexec(regex,line->d.id, 0,NULL,0) ? 0 : 1;
-        if ( rtok->tok_type==TOK_NLIKE ) rtok->pass_site = rtok->pass_site ? 0 : 1;
+            pass = khash_str2int_has_key(atok->hash, id);
+        }
+        else
+        {
+            if ( !btok->str_value.l ) error("Error occurred while evaluating the expression\n");
+
+            if ( rtok->tok_type==TOK_EQ || rtok->tok_type==TOK_NE )
+                pass = strcmp(btok->str_value.s,id) ? 0 : 1;
+            else
+            {
+                if ( rtok->tok_type!=TOK_LIKE && rtok->tok_type!=TOK_NLIKE )
+                    error("Only the following operators are supported for querying ID: ==, !=, ~, !~; the operator type %d is not supported (%p %p)\n",
+                            rtok->tok_type,atok->regex,btok->regex);
+
+                regex_t *regex = atok->regex ? atok->regex : (btok->regex ? btok->regex : NULL);
+                if ( !regex ) error("fixme: regex initialization failed\n");
+                pass = regexec(regex,id, 0,NULL,0) ? 0 : 1;
+            }
+        }
+        if ( ep )
+        {
+            *ep = ';';
+            id = ep + 1;
+        }
+        if ( pass || !ep ) break;
     }
+    if ( rtok->tok_type==TOK_NE || rtok->tok_type==TOK_NE) pass = pass ? 0 : 1;
+    rtok->pass_site = pass;
 }
 
 /**
@@ -639,7 +762,7 @@ static void filters_cmp_id(token_t *atok, token_t *btok, token_t *rtok, bcf1_t *
  *  @line:      BCF line
  *  @info_id:   tag ID, as returned by bcf_hdr_id2int
  *  @ivec:      0-based index to retrieve, -1 when single value is expected
- *  @vptr:      pointer to memory location of sufficient size to accomodate
+ *  @vptr:      pointer to memory location of sufficient size to accommodate
  *              info_id's type
  *
  *  The returned value is -1 if tag is not present, 0 if present but
@@ -661,22 +784,25 @@ static int bcf_get_info_value(bcf1_t *line, int info_id, int ivec, void *value)
     }
 
     if ( ivec<0 ) ivec = 0;
+    if ( ivec>=info->len) return 0;
 
-    #define BRANCH(type_t, is_missing, is_vector_end, out_type_t) { \
-        type_t *p = (type_t *) info->vptr; \
-        for (j=0; j<ivec && j<info->len; j++) \
+    #define BRANCH(type_t, convert, is_missing, is_vector_end, out_type_t) { \
+        uint8_t *p = info->vptr; \
+        for (j=0; j<ivec; j++) \
         { \
+            type_t val = convert(&p[j * sizeof(type_t)]); \
             if ( is_vector_end ) return 0; \
         } \
+        type_t val = convert(&p[ivec * sizeof(type_t)]); \
         if ( is_missing ) return 0; \
-        *((out_type_t*)value) = p[j]; \
+        *((out_type_t*)value) = val; \
         return 1; \
     }
     switch (info->type) {
-        case BCF_BT_INT8:  BRANCH(int8_t,  p[j]==bcf_int8_missing,  p[j]==bcf_int8_vector_end,  int64_t); break;
-        case BCF_BT_INT16: BRANCH(int16_t, p[j]==bcf_int16_missing, p[j]==bcf_int16_vector_end, int64_t); break;
-        case BCF_BT_INT32: BRANCH(int32_t, p[j]==bcf_int32_missing, p[j]==bcf_int32_vector_end, int64_t); break;
-        case BCF_BT_FLOAT: BRANCH(float,   bcf_float_is_missing(p[j]), bcf_float_is_vector_end(p[j]), double); break;
+        case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  int64_t); break;
+        case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, int64_t); break;
+        case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, int64_t); break;
+        case BCF_BT_FLOAT: BRANCH(float,   le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), double); break;
         default: fprintf(bcftools_stderr,"todo: type %d\n", info->type); bcftools_exit(1); break;
     }
     #undef BRANCH
@@ -877,6 +1003,7 @@ static void filters_set_format_int(filter_t *flt, bcf1_t *line, token_t *tok)
                 if ( !(flt->cached_GT.mask[i] & (1<<k)) ) continue;
                 dst[j++] = src[k];
             }
+            if ( !j ) { bcf_double_set_missing(dst[j]); j++; }
             for (; j<tok->nval1; j++) bcf_double_set_vector_end(dst[j]);
         }
     }
@@ -967,6 +1094,7 @@ static void filters_set_format_float(filter_t *flt, bcf1_t *line, token_t *tok)
                     dst[j] = src[k];
                 j++;
             }
+            if ( !j ) { bcf_double_set_missing(dst[j]); j++; }
             for (; j<tok->nval1; j++) bcf_double_set_vector_end(dst[j]);
         }
     }
@@ -1010,35 +1138,45 @@ static void filters_set_format_string(filter_t *flt, bcf1_t *line, token_t *tok)
 
     int i, ndim = tok->str_value.m;
     int nstr = bcf_get_format_char(flt->hdr, line, tok->tag, &tok->str_value.s, &ndim);
-    tok->str_value.m = ndim;
+    tok->str_value.m = tok->str_value.l = ndim;
+    kputc(0,&tok->str_value); // append the nul byte
     tok->str_value.l = tok->nvalues = 0;
 
     if ( nstr<0 ) return;
 
+    if ( tok->idx==-3 && filters_cache_genotypes(flt,line)!=0 ) return;
+
     tok->nvalues = tok->str_value.l = nstr;
     tok->nval1   = nstr / tok->nsamples;
     for (i=0; i<tok->nsamples; i++)
     {
         if ( !tok->usmpl[i] ) continue;
         char *src = tok->str_value.s + i*tok->nval1, *dst = src;
-        int ibeg = 0, idx = 0;
+        int ibeg = 0;
+        int idx = 0;        // idx-th field
         while ( ibeg < tok->nval1 )
         {
             int iend = ibeg;
             while ( iend < tok->nval1 && src[iend] && src[iend]!=',' ) iend++;
 
             int keep = 0;
-            if ( tok->idx >= 0 )
+            if ( tok->idx >= 0 )    // single index, given explicitly, e.g. AD[:1]
             {
                 if ( tok->idx==idx ) keep = 1;
             }
-            else if ( idx < tok->nidxs )
+            else if ( tok->idx == -3 )  // given by GT index, e.g. AD[:GT]
             {
-                if ( tok->idxs[idx] != 0 ) keep = 1;
+                if ( flt->cached_GT.mask[i] & (1<<idx) ) keep = 1;
+            }
+            else    // given as a list, e.g. AD[:0,3]
+            {
+                if ( idx < tok->nidxs )
+                {
+                    if ( tok->idxs[idx] != 0 ) keep = 1;
+                }
+                else if ( tok->idxs[tok->nidxs-1] < 0 )
+                    keep = 1;
             }
-            else if ( tok->idxs[tok->nidxs-1] < 0 )
-                keep = 1;
-
             if ( keep )
             {
                 if ( ibeg!=0 ) memmove(dst, src+ibeg, iend-ibeg+1);
@@ -1069,23 +1207,23 @@ static void _filters_set_genotype(filter_t *flt, bcf1_t *line, token_t *tok, int
         tok->str_value.s = (char*)realloc(tok->str_value.s, tok->str_value.m);
     }
 
-#define BRANCH_INT(type_t,vector_end) \
+#define BRANCH_INT(type_t, convert, vector_end) \
     { \
         for (i=0; i<line->n_sample; i++) \
         { \
-            type_t *ptr = (type_t*) (fmt->p + i*fmt->size); \
-            int is_het = 0, has_ref = 0, missing = 0; \
+            uint8_t *ptr = fmt->p + i*fmt->size; \
+            int is_het = 0, has_ref = 0, missing = 0, jal = 0; \
             for (j=0; j<fmt->n; j++) \
             { \
-                if ( ptr[j]==vector_end ) break; /* smaller ploidy */ \
-                if ( bcf_gt_is_missing(ptr[j]) ) { missing=1; break; } /* missing allele */ \
-                int ial = ptr[j]; \
+                type_t ial = convert(&ptr[j * sizeof(type_t)]); \
+                if ( ial==vector_end ) break; /* smaller ploidy */ \
+                if ( bcf_gt_is_missing(ial) ) { missing=1; break; } /* missing allele */ \
                 if ( bcf_gt_allele(ial)==0 ) has_ref = 1; \
                 if ( j>0 ) \
                 { \
-                    int jal = ptr[j-1]; \
                     if ( bcf_gt_allele(ial)!=bcf_gt_allele(jal) ) is_het = 1; \
                 } \
+                jal = ial; \
             } \
             char *dst = &tok->str_value.s[nvals1*i]; \
             if ( type==4 ) \
@@ -1123,9 +1261,9 @@ static void _filters_set_genotype(filter_t *flt, bcf1_t *line, token_t *tok, int
         } \
     }
     switch (fmt->type) {
-        case BCF_BT_INT8:  BRANCH_INT(int8_t,  bcf_int8_vector_end); break;
-        case BCF_BT_INT16: BRANCH_INT(int16_t, bcf_int16_vector_end); break;
-        case BCF_BT_INT32: BRANCH_INT(int32_t, bcf_int32_vector_end); break;
+        case BCF_BT_INT8:  BRANCH_INT(int8_t,  le_to_i8,  bcf_int8_vector_end); break;
+        case BCF_BT_INT16: BRANCH_INT(int16_t, le_to_i16, bcf_int16_vector_end); break;
+        case BCF_BT_INT32: BRANCH_INT(int32_t, le_to_i32, bcf_int32_vector_end); break;
         default: error("The GT type is not lineognised: %d at %s:%"PRId64"\n",fmt->type, bcf_seqname(flt->hdr,line),(int64_t) line->pos+1); break;
     }
 #undef BRANCH_INT
@@ -1182,9 +1320,13 @@ static void filters_set_ilen(filter_t *flt, bcf1_t *line, token_t *tok)
     int i, rlen = strlen(line->d.allele[0]);
     for (i=1; i<line->n_allele; i++)
     {
+        if ( line->d.allele[i][0]=='<' )
+        {
+            bcf_double_set_missing(tok->values[i-1]);
+            continue;
+        }
         int alen = strlen(line->d.allele[i]);
-        if ( rlen==alen ) bcf_double_set_missing(tok->values[i-1]);
-        else tok->values[i-1] = alen - rlen;
+        tok->values[i-1] = alen - rlen;
     }
 }
 static void filters_set_ref_string(filter_t *flt, bcf1_t *line, token_t *tok)
@@ -1249,21 +1391,22 @@ static void filters_set_nmissing(filter_t *flt, bcf1_t *line, token_t *tok)
     }
 
     int j,nmissing = 0;
-    #define BRANCH(type_t, is_vector_end) { \
+    #define BRANCH(type_t, convert, is_vector_end) { \
         for (i=0; i<line->n_sample; i++) \
         { \
-            type_t *ptr = (type_t *) (fmt->p + i*fmt->size); \
+            uint8_t *ptr = fmt->p + i*fmt->size; \
             for (j=0; j<fmt->n; j++) \
             { \
-                if ( ptr[j]==is_vector_end ) break; \
-                if ( ptr[j]==bcf_gt_missing ) { nmissing++; break; } \
+                type_t val = convert(&ptr[j * sizeof(type_t)]); \
+                if ( val==is_vector_end ) break; \
+                if ( val==bcf_gt_missing ) { nmissing++; break; } \
             } \
         } \
     }
     switch (fmt->type) {
-        case BCF_BT_INT8:  BRANCH(int8_t,  bcf_int8_vector_end); break;
-        case BCF_BT_INT16: BRANCH(int16_t, bcf_int16_vector_end); break;
-        case BCF_BT_INT32: BRANCH(int32_t, bcf_int32_vector_end); break;
+        case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  bcf_int8_vector_end); break;
+        case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, bcf_int16_vector_end); break;
+        case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, bcf_int32_vector_end); break;
         default: fprintf(bcftools_stderr,"todo: type %d\n", fmt->type); bcftools_exit(1); break;
     }
     #undef BRANCH
@@ -1895,19 +2038,6 @@ static int func_strlen(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **sta
     }
     return 1;
 }
-static inline double calc_binom(int na, int nb)
-{
-    if ( na==0 && nb==0 ) return -1;
-    if ( na==nb ) return 1;
-
-    // kfunc.h implements kf_betai, which is the regularized beta function  P(X<=k/N;p) = I_{1-p}(N-k,k+1)
-
-    double pval = na < nb ? kf_betai(nb, na + 1, 0.5) : kf_betai(na, nb + 1, 0.5);
-    pval *= 2;
-    if ( pval>1 ) pval = 1;     // this can happen, machine precision error, eg. kf_betai(1,0,0.5)
-
-    return pval;
-}
 static int func_binom(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **stack, int nstack)
 {
     int i, istack = nstack - rtok->nargs;
@@ -1975,7 +2105,7 @@ static int func_binom(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **stac
                     bcf_double_set_missing(rtok->values[i]);
                     continue;
                 }
-                rtok->values[i] = calc_binom(vals[idx1],vals[idx2]);
+                rtok->values[i] = calc_binom_two_sided(vals[idx1],vals[idx2],0.5);
                 if ( rtok->values[i] < 0 )
                 {
                     bcf_double_set_missing(rtok->values[i]);
@@ -1999,7 +2129,7 @@ static int func_binom(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **stac
                     bcf_double_set_missing(rtok->values[i]);
                     continue;
                 }
-                rtok->values[i] = calc_binom(ptr1[0],ptr2[0]);
+                rtok->values[i] = calc_binom_two_sided(ptr1[0],ptr2[0],0.5);
                 if ( rtok->values[i] < 0 )
                 {
                     bcf_double_set_missing(rtok->values[i]);
@@ -2038,7 +2168,7 @@ static int func_binom(filter_t *flt, bcf1_t *line, token_t *rtok, token_t **stac
             bcf_double_set_missing(rtok->values[0]);
         else
         {
-            rtok->values[0] = calc_binom(ptr1[0],ptr2[0]);
+            rtok->values[0] = calc_binom_two_sided(ptr1[0],ptr2[0],0.5);
             if ( rtok->values[0] < 0 )
                 bcf_double_set_missing(rtok->values[0]);
         }
@@ -2308,6 +2438,18 @@ static int vector_logic_and(filter_t *filter, bcf1_t *line, token_t *rtok, token
     return 2;
 }
 
+// A note about comparisons:
+// When setting value by determining index from the genotype, we face the problem
+// of how to interpret truncating arrays. Say we have TAG defined as Number=. and
+//      GT:TAG   1/1:0,1,2  0/0:0
+// Then when querying we expect the following expression to evaluate for the second
+// sample as
+//      -i 'TAG[1:1]="."'  .. true
+//      -i 'TAG[1:GT]="."' .. false
+// The problem is that the implementation truncates the number of fields, filling
+// usually fewer than the original number of per-sample values. This is fixed by
+// adding an exception that makes the code aware of this: the GT indexing can be
+// recognised by having tok->idx==-3
 #define CMP_VECTORS(atok,btok,_rtok,CMP_OP,missing_logic) \
 { \
     token_t *rtok = _rtok; \
@@ -2395,6 +2537,8 @@ static int vector_logic_and(filter_t *filter, bcf1_t *line, token_t *rtok, token
                 double *bptr = btok->values + i*btok->nval1; \
                 for (j=0; j<atok->nval1; j++) \
                 { \
+                    if ( atok->idx==-3 && bcf_double_is_vector_end(aptr[j]) ) break; /* explained above */ \
+                    if ( btok->idx==-3 && bcf_double_is_vector_end(bptr[j]) ) break; /* explained above */ \
                     int nmiss = bcf_double_is_missing_or_vector_end(aptr[j]) ? 1 : 0; \
                     if ( nmiss && !missing_logic[0] ) continue; /* any is missing => result is false */ \
                     nmiss += (bcf_double_is_missing_or_vector_end(bptr[j]) ? 1 : 0); \
@@ -2416,9 +2560,10 @@ static int vector_logic_and(filter_t *filter, bcf1_t *line, token_t *rtok, token
             { \
                 if ( !rtok->usmpl[i] ) continue; \
                 double *aptr = atok->values + i*atok->nval1; \
-                double *bptr = btok->values + i*btok->nval1; \
+                double *bptr = btok->values; \
                 for (j=0; j<atok->nval1; j++) \
                 { \
+                    if ( atok->idx==-3 && bcf_double_is_vector_end(aptr[j]) ) break; /* explained above */ \
                     int miss = bcf_double_is_missing_or_vector_end(aptr[j]) ? 1 : 0; \
                     if ( miss && !missing_logic[0] ) continue; /* any is missing => result is false */ \
                     for (k=0; k<btok->nvalues; k++) \
@@ -2442,10 +2587,11 @@ static int vector_logic_and(filter_t *filter, bcf1_t *line, token_t *rtok, token
             for (i=0; i<btok->nsamples; i++) \
             { \
                 if ( !rtok->usmpl[i] ) continue; \
-                double *aptr = atok->values + i*atok->nval1; \
+                double *aptr = atok->values; \
                 double *bptr = btok->values + i*btok->nval1; \
                 for (j=0; j<btok->nval1; j++) \
                 { \
+                    if ( atok->idx==-3 && bcf_double_is_vector_end(bptr[j]) ) break; /* explained above */ \
                     int miss = bcf_double_is_missing_or_vector_end(bptr[j]) ? 1 : 0; \
                     if ( miss && !missing_logic[0] ) continue; /* any is missing => result is false */ \
                     for (k=0; k<atok->nvalues; k++) \
@@ -2809,9 +2955,26 @@ static int max_ac_an_unpack(bcf_hdr_t *hdr)
 
     return BCF_UN_INFO;
 }
+static int filters_init1_ext(filter_t *filter, char *str, int len, token_t *tok)
+{
+    tok->hl_type  = -1;
+    tok->ht_type  = -1;
+    tok->tok_type  = TOK_VAL;
+    tok->hdr_id    = -1;
+    tok->pass_site = -1;
+    tok->idx       = 0;
+    tok->iext = ++filter->n_ext;
+    filter->ext = realloc(filter->ext,sizeof(*filter->ext)*filter->n_ext);
+    if ( !strncasecmp(str,"{str}",len) ) { tok->ht_type = BCF_HT_STR; tok->is_str = 1; }
+    else if ( !strncasecmp(str,"{int}",len) ) tok->ht_type = BCF_HT_INT;
+    else if ( !strncasecmp(str,"{float}",len) ) tok->ht_type = BCF_HT_REAL;
+    filter->ext[filter->n_ext-1] = tok->ht_type;
+    return 0;
+}
 static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
 {
-    tok->tag_type  = -1;
+    tok->ht_type  = -1;
+    tok->hl_type  = -1;
     tok->tok_type  = TOK_VAL;
     tok->hdr_id    = -1;
     tok->pass_site = -1;
@@ -2828,6 +2991,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         memcpy(tok->key,str+1,len-2);
         tok->key[len-2] = 0;
         tok->is_str = 1;
+        tok->ht_type = BCF_HT_STR;
         tok->nvalues = len-2;
         if ( !strcmp(".",tok->key) ) tok->is_missing = 1;
         return 0;
@@ -2869,6 +3033,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = filters_set_qual;
             tok->tag = strdup("QUAL");
+            tok->ht_type = BCF_HT_REAL;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2876,6 +3041,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = filters_set_type;
             tok->tag = strdup("TYPE");
+            tok->ht_type = BCF_HT_STR;
             return 0;
         }
         else if ( !strncasecmp(str,"FILTER",len) || !strncmp(str,"%FILTER",len) /* for backward compatibility */ )
@@ -2883,7 +3049,8 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             tok->comparator = filters_cmp_filter;
             tok->tag = strdup("FILTER");
             filter->max_unpack |= BCF_UN_FLT;
-            tok->tag_type = BCF_HL_FLT;
+            tok->hl_type = BCF_HL_FLT;
+            tok->ht_type = BCF_HT_STR;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2891,6 +3058,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->comparator = filters_cmp_id;
             tok->tag = strdup("ID");
+            tok->ht_type = BCF_HT_STR;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2898,6 +3066,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = &filters_set_chrom;
             tok->tag = strdup("CHROM");
+            tok->ht_type = BCF_HT_STR;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2905,6 +3074,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = &filters_set_pos;
             tok->tag = strdup("POS");
+            tok->ht_type = BCF_HT_INT;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2913,6 +3083,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             tok->setter = &filters_set_ref_string;
             tok->is_str = 1;
             tok->tag = strdup("REF");
+            tok->ht_type = BCF_HT_STR;
             filter_add_used_tag(filter,NULL,tok->tag);
             return 0;
         }
@@ -2921,6 +3092,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             tok->setter = &filters_set_alt_string;
             tok->is_str = 1;
             tok->tag = strdup("ALT");
+            tok->ht_type = BCF_HT_STR;
             tok->idxs = (int*) malloc(sizeof(int));
             tok->idxs[0] = -1;
             tok->nidxs   = 1;
@@ -2932,6 +3104,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         {
             tok->setter = &filters_set_nalt;
             tok->tag = strdup("N_ALT");
+            tok->ht_type = BCF_HT_INT;
             return 0;
         }
         else if ( !strncasecmp(str,"N_SAMPLES",len) )
@@ -2939,6 +3112,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             tok->tok_type = TOK_VAL;
             tok->threshold = bcf_hdr_nsamples(filter->hdr);
             tok->is_constant = 1;
+            tok->ht_type = BCF_HT_INT;
             return 0;
         }
         else if ( !strncasecmp(str,"N_MISSING",len) )
@@ -2946,6 +3120,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             filter->max_unpack |= BCF_UN_FMT;
             tok->setter = &filters_set_nmissing;
             tok->tag = strdup("N_MISSING");
+            tok->ht_type = BCF_HT_INT;
             return 0;
         }
         else if ( !strncasecmp(str,"F_MISSING",len) )
@@ -2953,6 +3128,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             filter->max_unpack |= BCF_UN_FMT;
             tok->setter = &filters_set_nmissing;
             tok->tag = strdup("F_MISSING");
+            tok->ht_type = BCF_HT_REAL;
             return 0;
         }
     }
@@ -2993,13 +3169,14 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         for (i=0; i<tok->nsamples; i++) tok->usmpl[i] = 1;
     }
 
-    tok->tag_type = is_fmt ? BCF_HL_FMT : BCF_HL_INFO;
+    tok->hl_type = is_fmt ? BCF_HL_FMT : BCF_HL_INFO;
     if ( is_fmt ) filter->max_unpack |= BCF_UN_FMT;
     if ( tok->hdr_id>=0 )
     {
         if ( is_fmt && !strcmp("GT",tmp.s) )
         {
             tok->setter = &filters_set_genotype_string; tok->is_str = 1;
+            tok->ht_type = BCF_HT_STR;
         }
         else if ( is_fmt )
         {
@@ -3014,9 +3191,9 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             }
             switch ( bcf_hdr_id2type(filter->hdr,BCF_HL_FMT,tok->hdr_id) )
             {
-                case BCF_HT_INT:  tok->setter = &filters_set_format_int; break;
-                case BCF_HT_REAL: tok->setter = &filters_set_format_float; break;
-                case BCF_HT_STR:  tok->setter = &filters_set_format_string; tok->is_str = 1; break;
+                case BCF_HT_INT:  tok->setter = &filters_set_format_int; tok->ht_type = BCF_HT_INT; break;
+                case BCF_HT_REAL: tok->setter = &filters_set_format_float; tok->ht_type = BCF_HT_REAL; break;
+                case BCF_HT_STR:  tok->setter = &filters_set_format_string; tok->ht_type = BCF_HT_STR; tok->is_str = 1; break;
                 default: error("[%s:%d %s] FIXME\n", __FILE__,__LINE__,__FUNCTION__);
             }
         }
@@ -3025,10 +3202,14 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         else
         {
             if ( bcf_hdr_id2type(filter->hdr,BCF_HL_INFO,tok->hdr_id) == BCF_HT_FLAG )
+            {
                 tok->setter = filters_set_info_flag;
+                tok->ht_type = BCF_HT_INT;
+            }
             else
             {
-                if ( bcf_hdr_id2type(filter->hdr,BCF_HL_INFO,tok->hdr_id) == BCF_HT_STR ) tok->is_str = 1;
+                tok->ht_type = bcf_hdr_id2type(filter->hdr,BCF_HL_INFO,tok->hdr_id);
+                if ( tok->ht_type == BCF_HT_STR ) tok->is_str = 1;
                 if ( bcf_hdr_id2number(filter->hdr,BCF_HL_INFO,tok->hdr_id)==1 )
                     tok->setter = filters_set_info;
                 else
@@ -3060,6 +3241,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
     {
         tok->setter = &filters_set_alt_string;
         tok->is_str = 1;
+        tok->ht_type = BCF_HT_STR;
         tok->tag = strdup(tmp.s);
         free(tmp.s);
         filter_add_used_tag(filter,NULL,tok->tag);
@@ -3070,6 +3252,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= BCF_UN_FMT;
         tok->setter = &filters_set_an;
         tok->tag = strdup("AN");
+        tok->ht_type = BCF_HT_INT;
         free(tmp.s);
         return 0;
     }
@@ -3078,6 +3261,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= BCF_UN_FMT;
         tok->setter = &filters_set_ac;
         tok->tag = strdup("AC");
+        tok->ht_type = BCF_HT_INT;
         free(tmp.s);
         return 0;
     }
@@ -3086,6 +3270,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= max_ac_an_unpack(filter->hdr);
         tok->setter = &filters_set_mac;
         tok->tag = strdup("MAC");
+        tok->ht_type = BCF_HT_INT;
         free(tmp.s);
         return 0;
     }
@@ -3094,6 +3279,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= max_ac_an_unpack(filter->hdr);
         tok->setter = &filters_set_af;
         tok->tag = strdup("AF");
+        tok->ht_type = BCF_HT_REAL;
         free(tmp.s);
         return 0;
     }
@@ -3102,6 +3288,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= max_ac_an_unpack(filter->hdr);
         tok->setter = &filters_set_maf;
         tok->tag = strdup("MAF");
+        tok->ht_type = BCF_HT_REAL;
         free(tmp.s);
         return 0;
     }
@@ -3110,6 +3297,7 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
         filter->max_unpack |= BCF_UN_STR;
         tok->setter = &filters_set_ilen;
         tok->tag = strdup("ILEN");
+        tok->ht_type = BCF_HT_INT;
         free(tmp.s);
         return 0;
     }
@@ -3132,7 +3320,10 @@ static int filters_init1(filter_t *filter, char *str, int len, token_t *tok)
             filter->status |= FILTER_ERR_UNKN_TAGS;
             filter_add_undef_tag(filter,tmp.s);
         }
+        tok->ht_type = BCF_HT_REAL;
     }
+    else
+        tok->ht_type = BCF_HT_INT;
     tok->is_constant = 1;
 
     if ( tmp.s ) free(tmp.s);
@@ -3286,6 +3477,33 @@ static void perl_destroy(filter_t *filter)
 #endif
 }
 
+// A very rudimentary heuristics to determine type, e.g. STR_TAG={} implies {str}.
+// Throws an error on anything more complex and asks for an explicit type.
+static void determine_ext_types(filter_t *filter, int ntok, token_t *tok)
+{
+    int i;
+    for (i=0; i<ntok; i++)
+    {
+        if ( !tok[i].iext || tok[i].ht_type!=-1 ) continue;
+        if ( !i || i+1==ntok ) break;       // first or last in the RPN
+        if ( tok[i-1].ht_type==-1 ) break;  // previous type not set
+        // todo: check if the next is an operator
+        // todo: case when the order is reversed, {}=TAG
+        tok[i].ht_type = tok[i-1].ht_type;
+    }
+    if ( i!=ntok )
+        error("[%s:%d %s] Error: unable to determine the type, use explicit notation: %s\n",__FILE__,__LINE__,__FUNCTION__,filter->str);
+    for (i=0; i<ntok; i++)
+    {
+        int j = tok[i].iext - 1;
+        if ( j<0 ) continue;
+        if ( filter->ext[j]!=-1 && filter->ext[j]!=tok[i].ht_type  )
+            error("[%s:%d %s] FIXME: this should not happen %d vs %d, iext=%d\n",__FILE__,__LINE__,__FUNCTION__,filter->ext[j],tok[i].ht_type,j);
+        filter->ext[j] = tok[i].ht_type;
+        if ( tok[i].ht_type==BCF_HT_STR ) tok[i].is_str = 1;
+    }
+}
+
 
 // Parse filter expression and convert to reverse polish notation. Dijkstra's shunting-yard algorithm
 static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error)
@@ -3333,6 +3551,13 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
             memset(&ops[nops-1],0,sizeof(token_t));
             nops--;
         }
+        else if ( ret==TOK_EXT )    // external value
+        {
+            nout++;
+            hts_expand0(token_t, nout, mout, out);
+            filters_init1_ext(filter, tmp, len, &out[nout-1]);
+            tmp += len;
+        }
         else if ( ret!=TOK_VAL )    // one of the operators
         {
             // detect unary minus: replace -value with -1*(value)
@@ -3443,12 +3668,12 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
             hts_expand0(token_t, nops, mops, ops);
             ops[nops-1].tok_type = ret;
         }
-        else if ( !len )
+        else if ( !len )    // all tokes read or an error
         {
             if ( *tmp && !isspace(*tmp) ) error("Could not parse the expression: [%s]\n", str);
             break;     // all tokens read
         }
-        else           // annotation name or filtering value
+        else           // TOK_VAL: annotation name or value
         {
             nout++;
             hts_expand0(token_t, nout, mout, out);
@@ -3470,10 +3695,21 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
         nops--;
     }
 
+    if ( filter->status != FILTER_OK )
+    {
+        if ( mops ) free(ops);
+        filter->filters   = out;
+        filter->nfilters  = nout;
+        return filter;
+    }
+
+    // Determine types of external variables from the context
+    determine_ext_types(filter,nout,out);
+
     // In the special cases of TYPE and FILTER the BCF header IDs are yet unknown. Walk through the
     // list of operators and convert the strings (e.g. "PASS") to BCF ids. The string value token must be
     // just before or after the FILTER token and they must be followed with a comparison operator.
-    // At this point we also initialize regex expressions which, in RPN, must preceed the LIKE/NLIKE operator.
+    // At this point we also initialize regex expressions which, in RPN, must precede the LIKE/NLIKE operator.
     // Additionally, treat "." as missing value rather than a string in numeric equalities; that
     // @file is only used with ID; etc.
     // This code is fragile: improve me.
@@ -3488,7 +3724,10 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
         {
             int j = out[i+1].tok_type==TOK_VAL ? i+1 : i-1;
             if ( out[j].comparator!=filters_cmp_id )
-                error("Error: could not parse the expression. Note that the \"@file_name\" syntax can be currently used with ID column only.\n");
+            {
+                if ( out[j].comparator ) error("Error: could not parse the expression with \"@file_name\" syntax (possible todo)\n");
+                out[j].comparator = filters_cmp_string_hash;
+            }
         }
         if ( out[i].tok_type==TOK_OR || out[i].tok_type==TOK_OR_VEC )
             out[i].func = vector_logic_or;
@@ -3504,7 +3743,7 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
                 int set_missing = 0;
                 if ( out[k].hdr_id>0 )
                 {
-                    int type = bcf_hdr_id2type(filter->hdr,out[k].tag_type,out[k].hdr_id);
+                    int type = bcf_hdr_id2type(filter->hdr,out[k].hl_type,out[k].hdr_id);
                     if ( type==BCF_HT_INT ) set_missing = 1;
                     else if ( type==BCF_HT_REAL ) set_missing = 1;
                 }
@@ -3545,7 +3784,8 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
         if ( !out[i].tag ) continue;
         if ( out[i].setter==filters_set_type )
         {
-            if ( i+1==nout ) error("Could not parse the expression: %s\n", filter->str);
+            if ( i+1==nout || !out[i+1].key )
+                error("Could not parse the expression: %s\n", filter->str);
             int itok, ival;
             if ( out[i+1].tok_type==TOK_EQ || out[i+1].tok_type==TOK_NE ) ival = i - 1, itok = i + 1;
             else if ( out[i+1].tok_type==TOK_LIKE || out[i+1].tok_type==TOK_NLIKE ) ival = i - 1, itok = i + 1;
@@ -3593,7 +3833,7 @@ static filter_t *filter_init_(bcf_hdr_t *hdr, const char *str, int exit_on_error
             else if ( !strcasecmp(out[ival].key,"r") ) { out[i].setter = filters_set_genotype2; out[ival].key[0]='r'; out[ival].key[1]=0; }  // r
             continue;
         }
-        if ( out[i].tag_type==BCF_HL_FLT )
+        if ( out[i].hl_type==BCF_HL_FLT )
         {
             if ( i+1==nout ) error("Could not parse the expression: %s\n", filter->str);
             int itok = i, ival;
@@ -3707,6 +3947,7 @@ void filter_destroy(filter_t *filter)
     }
     for (i=0; i<filter->nundef_tag; i++) free(filter->undef_tag[i]);
     for (i=0; i<filter->nused_tag; i++) free(filter->used_tag[i]);
+    free(filter->ext);
     free(filter->undef_tag);
     free(filter->used_tag);
     free(filter->cached_GT.buf);
@@ -3720,6 +3961,37 @@ void filter_destroy(filter_t *filter)
     free(filter);
 }
 
+int filter_test_ext(filter_t *filter, bcf1_t *rec, const uint8_t **samples, const void **ext)
+{
+    if ( !filter->n_ext )
+        return filter_test(filter,rec,samples);
+
+    int i;
+    for (i=0; i<filter->nfilters; i++)
+    {
+        token_t *tok = &filter->filters[i];
+        if ( !tok->iext ) continue;
+        if ( !ext[tok->iext-1] )
+        {
+            tok->is_missing = 1;
+            tok->nvalues = 0;
+            if ( filter->ext[tok->iext-1]==BCF_HT_STR ) tok->str_value.l = 0;
+            continue;
+        }
+        tok->is_missing = 0;
+        tok->nvalues = 1;
+        if ( filter->ext[tok->iext-1]==BCF_HT_STR )
+        {
+            tok->str_value.l = 0;
+            kputs((const char*)ext[tok->iext-1],&tok->str_value);
+            tok->nvalues = tok->str_value.l;
+        }
+        else if ( filter->ext[tok->iext-1]==BCF_HT_INT ) tok->values[0] = *((const int*)ext[tok->iext-1]);
+        else if ( filter->ext[tok->iext-1]==BCF_HT_REAL ) tok->values[0] = *((const float*)ext[tok->iext-1]);
+    }
+    return filter_test(filter,rec,samples);
+}
+
 int filter_test(filter_t *filter, bcf1_t *line, const uint8_t **samples)
 {
     if ( filter->status != FILTER_OK ) error("Error: the caller did not check the filter status\n");
@@ -3856,7 +4128,11 @@ int filter_max_unpack(filter_t *flt)
 {
     return flt->max_unpack;
 }
-
+const int *filter_ext_types(filter_t *filter, int *n_ext)
+{
+    *n_ext = filter->n_ext;
+    return filter->ext;
+}
 const double *filter_get_doubles(filter_t *filter, int *nval, int *nval1)
 {
     token_t *tok = filter->flt_stack[0];
index cc60d6b96547a061d46baa4b90a48f7a59fe5a32..d6a8e089309f77cc290d4c8e60fed82f36d4f6d7 100644 (file)
@@ -1,6 +1,6 @@
 /*  filter.h -- filter expressions.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -31,8 +31,9 @@ typedef struct _filter_t filter_t;
 
 /**
   *  @hdr:  BCF header file
-  *  @str:  see the bcftools filter command help for description
-  *
+  *  @str:  see the bcftools filter command help for description.
+  *         See also the extended usage described in filter_test_ext(),
+  *         intended for programmatic access
   *  Same as filter_parse() but exits on errors
   */
 filter_t *filter_init(bcf_hdr_t *hdr, const char *str);
@@ -48,6 +49,22 @@ void filter_destroy(filter_t *filter);
   */
 int filter_test(filter_t *filter, bcf1_t *rec, const uint8_t **samples);
 
+/**
+  *  filter_test_ext() - same as filter_test(), but sets some of the terms
+  *        on the fly. An expression initialized with, say,
+  *        "STR_TAG={} | INT_TAG={} | FLT_TAG={}" takes three
+  *        additional pointer arguments which are expected to point to memory
+  *        area occupied by the appropriate type, see also filter_ext_types().
+  *        The type determination is not fool-proof, in such case the type can
+  *        be given explicitly as eg "TAG={str}".
+  *  @ext: array of size 'n_ext' occupied with pointers to the data types
+  *        inferred from the expression given at the time of initialization.
+  *        The pointers set to NULL will be treated as if missing value "."
+  *        was given.
+  *  @n_ext: the size of 'ext' array
+  */
+int filter_test_ext(filter_t *filter, bcf1_t *rec, const uint8_t **samples, const void **ext);
+
 /**
   *  filter_set_samples() - restrict filtering expression to samples.
   *             Call after filter_init().
@@ -60,9 +77,14 @@ void filter_set_samples(filter_t *filter, const uint8_t *samples);
   */
 const double *filter_get_doubles(filter_t *filter, int *nval, int *nval1);
 
-void filter_expression_info(FILE *fp);
 int filter_max_unpack(filter_t *filter);
 
+/**
+  *  filter_ext_types() - returns the number and BCF_HT_* types of external values
+  *         found in the filtering expression
+  */
+const int *filter_ext_types(filter_t *filter, int *n_ext);
+
 /**
   *  Same as filter_init() but may not exit on some type of errors. The caller
   *  must check if the returned value is not NULL and if the consequent call
index 90da84ba9ab821a67ea3a94fb4feefcd291fab99..283ced331d22baceb919d97df99411524a22dfde 100644 (file)
    THE SOFTWARE.
 */
 
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <inttypes.h>
+#include <string.h>
+#include <strings.h>
+#include <htslib/hts.h>
+#include <htslib/khash.h>
+#include <htslib/khash_str2int.h>
+#include <htslib/kseq.h>
+#include <htslib/bgzf.h>
+#include <errno.h>
+#include "bcftools.h"
+#include "regidx.h"
 #include "gff.h"
 
 /*
@@ -39,9 +54,9 @@ typedef struct
     uint32_t beg;
     uint32_t end;
     uint32_t trid;
-    uint32_t strand:1;  // STRAND_REV,STRAND_FWD
+    uint32_t strand:2;  // STRAND_{REV,FWD,UNK}
     uint32_t phase:2;   // 0, 1, 2, or 3 for unknown
-    uint32_t iseq:29;
+    uint32_t iseq:28;
 }
 ftr_t;
 
@@ -460,13 +475,13 @@ static void gff_parse_exon(gff_t *gff, const char *line, ftr_t *ftr)
     // associate with transcript id
     gff_id_register(&gff->tscript_ids, aux->parent, aux->parent_end, &ftr->trid);
 
-    if ( ftr->strand==-1 && gff->verbosity > 0 )
+    if ( ftr->strand==STRAND_UNK && gff->verbosity > 0 )
     {
         if ( !gff->warned.unknown_strand || gff->verbosity > 1 )
             fprintf(stderr,"Warning: Ignoring GFF feature with unknown strand .. %s\n",line);
         gff->warned.unknown_strand++;
     }
-    if ( ftr->phase==-1 && gff->verbosity > 0 )
+    if ( ftr->phase==CDS_PHASE_UNKN && gff->verbosity > 0 )
     {
         if ( !gff->warned.unknown_phase|| gff->verbosity > 1 )
             fprintf(stderr,"Warning: Ignoring GFF feature with unknown phase .. %s\n",line);
@@ -507,8 +522,8 @@ static void gff_parse_gene(gff_t *gff, const char *line, ftr_t *ftr)
         gene->name = strdup(aux->gene_ids.str[gene_id]); // Name=<GeneName> field is not present, use the gene ID instead
 }
 
-// Returns 0 for exons,CDS,UTRs to indiciate these need to be pruned later and regidx built on them,
-// or -1 to indiciate the structure needs not be saved (either because of an error or because saved
+// Returns 0 for exons,CDS,UTRs to indicate these need to be pruned later and regidx built on them,
+// or -1 to indicate the structure needs not be saved (either because of an error or because saved
 // as transcript or gene.)
 static int gff_parse_line(gff_t *gff, char *line, ftr_t *ftr)
 {
@@ -554,10 +569,11 @@ static int gff_parse_line(gff_t *gff, char *line, ftr_t *ftr)
     ftr->strand = -1;
     if ( *ss == '+' ) ftr->strand = STRAND_FWD;
     else if ( *ss == '-' ) ftr->strand = STRAND_REV;
+    else ftr->strand = STRAND_UNK;
     ss += 2;
 
     // 8th column: phase (codon offset)
-    ftr->phase = -1;
+    ftr->phase = CDS_PHASE_UNKN;
     if ( *ss == '0' ) ftr->phase = 0;
     else if ( *ss == '1' ) ftr->phase = 1;
     else if ( *ss == '2' ) ftr->phase = 2;
@@ -727,12 +743,12 @@ static void tscript_init_cds(gff_t *gff)
                 if ( phase!=len%3 )
                 {
                     if ( !gff->force )
-                        error("Error: GFF3 assumption failed for transcript %s, CDS=%d: phase!=len%%3 (phase=%d, len=%d). Use the --force option to proceed anyway (at your own risk).\n",
+                        error("Error: GFF3 assumption failed for transcript %s, CDS=%"PRIu32": phase!=len%%3 (phase=%d, len=%d). Use the --force option to proceed anyway (at your own risk).\n",
                                 gff->tscript_ids.str[tr->id],tr->cds[i]->beg+1,phase,len);
                     if ( gff->verbosity > 0 )
                     {
                         if ( !gff->warned.wrong_phase || gff->verbosity > 1 )
-                            fprintf(stderr,"Warning: The GFF has inconsistent phase column in transcript %s, skipping. CDS pos=%d: phase!=len%%3 (phase=%d, len=%d)\n",
+                            fprintf(stderr,"Warning: The GFF has inconsistent phase column in transcript %s, skipping. CDS pos=%"PRIu32": phase!=len%%3 (phase=%d, len=%d)\n",
                                     gff->tscript_ids.str[tr->id],tr->cds[i]->beg+1,phase,len);
                         gff->warned.wrong_phase++;
                     }
@@ -743,7 +759,7 @@ static void tscript_init_cds(gff_t *gff)
             }
             if ( !tscript_ok ) continue;    // skip this transcript
         }
-        else
+        else if ( tr->strand==STRAND_REV )
         {
             if ( tr->cds[tr->ncds-1]->phase != CDS_PHASE_UNKN )
             {
@@ -790,12 +806,12 @@ static void tscript_init_cds(gff_t *gff)
                 if ( phase!=len%3 )
                 {
                     if ( !gff->force )
-                        error("Error: GFF3 assumption failed for transcript %s, CDS=%d: phase!=len%%3 (phase=%d, len=%d). Use the --force option to proceed anyway (at your own risk).\n",
+                        error("Error: GFF3 assumption failed for transcript %s, CDS=%"PRIu32": phase!=len%%3 (phase=%d, len=%d). Use the --force option to proceed anyway (at your own risk).\n",
                                 gff->tscript_ids.str[tr->id],tr->cds[i]->beg+1,phase,len);
                     if ( gff->verbosity > 0 )
                     {
                         if ( !gff->warned.wrong_phase || gff->verbosity > 1 )
-                            fprintf(stderr,"Warning: The GFF has inconsistent phase column in transcript %s, skipping. CDS pos=%d: phase!=len%%3 (phase=%d, len=%d)\n",
+                            fprintf(stderr,"Warning: The GFF has inconsistent phase column in transcript %s, skipping. CDS pos=%"PRIu32": phase!=len%%3 (phase=%d, len=%d)\n",
                                     gff->tscript_ids.str[tr->id],tr->cds[i]->beg+1,phase,len);
                         gff->warned.wrong_phase++;
                     }
@@ -806,6 +822,8 @@ static void tscript_init_cds(gff_t *gff)
             }
             if ( !tscript_ok ) continue;    // skip this transcript
         }
+        else
+            continue;   // unknown strand
 
         // set len. At the same check that CDS within a transcript do not overlap
         len = 0;
@@ -854,7 +872,7 @@ static void tscript_init_cds(gff_t *gff)
                     i--;
                 }
             }
-            else
+            else if ( tr->strand==STRAND_REV )
             {
                 i = 0;
                 while ( i<tr->ncds && len%3 )
@@ -896,7 +914,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         gf_gene_t *gene = (gf_gene_t*) kh_val(gff->init.gid2gene, k);
         char *gene_id = gff->init.gene_ids.str[gene->id];
         str.l = 0;
-        ksprintf(&str,"%s\t.\tgene\t%d\t%d\t.\t%c\t.\tID=%s;Name=%s;used=%d\n",gff->init.seq[gene->iseq],gene->beg+1,gene->end+1,gene->strand==STRAND_FWD?'+':'-',gene_id,gene->name,gene->used);
+        ksprintf(&str,"%s\t.\tgene\t%"PRIu32"\t%"PRIu32"\t.\t%c\t.\tID=%s;Name=%s;used=%d\n",gff->init.seq[gene->iseq],gene->beg+1,gene->end+1,gene->strand==STRAND_FWD?'+':(gene->strand==STRAND_REV?'-':'.'),gene_id,gene->name,gene->used);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
 
@@ -907,7 +925,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         char *gene_id =  gff->init.gene_ids.str[tr->gene->id];
         const char *type = tr->type==GF_PROTEIN_CODING ? "mRNA" : gf_type2gff_string(tr->type);
         str.l = 0;
-        ksprintf(&str,"%s\t.\t%s\t%d\t%d\t.\t%c\t.\tID=%s;Parent=%s;biotype=%s;used=%d\n",itr->seq,type,itr->beg+1,itr->end+1,tr->strand==STRAND_FWD?'+':'-',gff->tscript_ids.str[tr->id],gene_id,gf_type2gff_string(tr->type),tr->used);
+        ksprintf(&str,"%s\t.\t%s\t%"PRIu32"\t%"PRIu32"\t.\t%c\t.\tID=%s;Parent=%s;biotype=%s;used=%d\n",itr->seq,type,itr->beg+1,itr->end+1,tr->strand==STRAND_FWD?'+':(tr->strand==STRAND_REV?'-':'.'),gff->tscript_ids.str[tr->id],gene_id,gf_type2gff_string(tr->type),tr->used);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
     regitr_destroy(itr);
@@ -918,7 +936,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         gf_cds_t *cds = regitr_payload(itr,gf_cds_t*);
         gf_tscript_t *tr = cds->tr;
         str.l = 0;
-        ksprintf(&str,"%s\t.\tCDS\t%d\t%d\t.\t%c\t%c\tParent=%s\n",itr->seq,cds->beg+1,cds->beg+cds->len,tr->strand==STRAND_FWD?'+':'-',cds->phase==3?'.':cds->phase+(int)'0',gff->tscript_ids.str[tr->id]);
+        ksprintf(&str,"%s\t.\tCDS\t%"PRIu32"\t%"PRIu32"\t.\t%c\t%c\tParent=%s\n",itr->seq,cds->beg+1,cds->beg+cds->len,tr->strand==STRAND_FWD?'+':(tr->strand==STRAND_REV?'-':'.'),cds->phase==3?'.':cds->phase+(int)'0',gff->tscript_ids.str[tr->id]);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
     regitr_destroy(itr);
@@ -929,7 +947,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         gf_utr_t *utr = regitr_payload(itr,gf_utr_t*);
         gf_tscript_t *tr = utr->tr;
         str.l = 0;
-        ksprintf(&str,"%s\t.\t%s_prime_UTR\t%d\t%d\t.\t%c\t.\tParent=%s\n",itr->seq,utr->which==prime3?"three":"five",utr->beg+1,utr->end+1,tr->strand==STRAND_FWD?'+':'-',gff->tscript_ids.str[tr->id]);
+        ksprintf(&str,"%s\t.\t%s_prime_UTR\t%"PRIu32"\t%"PRIu32"\t.\t%c\t.\tParent=%s\n",itr->seq,utr->which==prime3?"three":"five",utr->beg+1,utr->end+1,tr->strand==STRAND_FWD?'+':(tr->strand==STRAND_REV?'-':'.'),gff->tscript_ids.str[tr->id]);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
     regitr_destroy(itr);
@@ -940,7 +958,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         gf_exon_t *exon = regitr_payload(itr,gf_exon_t*);
         gf_tscript_t *tr = exon->tr;
         str.l = 0;
-        ksprintf(&str,"%s\t.\texon\t%d\t%d\t.\t%c\t.\tParent=%s\n",itr->seq,exon->beg+1,exon->end+1,tr->strand==STRAND_FWD?'+':'-',gff->tscript_ids.str[tr->id]);
+        ksprintf(&str,"%s\t.\texon\t%"PRIu32"\t%"PRIu32"\t.\t%c\t.\tParent=%s\n",itr->seq,exon->beg+1,exon->end+1,tr->strand==STRAND_FWD?'+':(tr->strand==STRAND_REV?'-':'.'),gff->tscript_ids.str[tr->id]);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
     regitr_destroy(itr);
@@ -1004,7 +1022,7 @@ int gff_parse(gff_t *gff)
         else if ( ftr->type==GF_UTR5 ) register_utr(gff, ftr);
         else if ( ftr->type==GF_UTR3 ) register_utr(gff, ftr);
         else
-            error("something: %s\t%d\t%d\t%s\t%s\n", aux->seq[ftr->iseq],ftr->beg+1,ftr->end+1,gff->tscript_ids.str[ftr->trid],gf_type2gff_string(ftr->type));
+            error("something: %s\t%"PRIu32"\t%"PRIu32"\t%s\t%s\n", aux->seq[ftr->iseq],ftr->beg+1,ftr->end+1,gff->tscript_ids.str[ftr->trid],gf_type2gff_string(ftr->type));
     }
     tscript_init_cds(gff);
 
@@ -1046,7 +1064,7 @@ int gff_parse(gff_t *gff)
         INC_NWARN(wrong_phase);
         INC_NWARN(overlapping_cds);
         if ( nwarn > 0 )
-            fprintf(stderr,"Warning: %d warnings were supressed, run with `--verbose 2` to see them all\n",nwarn);
+            fprintf(stderr,"Warning: %d warnings were suppressed, increase verbosity to see them all\n",nwarn);
     }
 
     if ( gff->dump_fname ) gff_dump(gff, gff->dump_fname);
index f5c817d73cbee80fdee456f46db809abb6c7b809..3722f606c3d5b3913bef9286650f0d1dca70a898 100644 (file)
    THE SOFTWARE.
 */
 
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <inttypes.h>
+#include <string.h>
+#include <strings.h>
+#include <htslib/hts.h>
+#include <htslib/khash.h>
+#include <htslib/khash_str2int.h>
+#include <htslib/kseq.h>
+#include <htslib/bgzf.h>
+#include <errno.h>
+#include "bcftools.h"
+#include "regidx.h"
 #include "gff.h"
 
 /*
@@ -41,9 +56,9 @@ typedef struct
     uint32_t beg;
     uint32_t end;
     uint32_t trid;
-    uint32_t strand:1;  // STRAND_REV,STRAND_FWD
+    uint32_t strand:2;  // STRAND_{REV,FWD,UNK}
     uint32_t phase:2;   // 0, 1, 2, or 3 for unknown
-    uint32_t iseq:29;
+    uint32_t iseq:28;
 }
 ftr_t;
 
@@ -462,13 +477,13 @@ static void gff_parse_exon(gff_t *gff, const char *line, ftr_t *ftr)
     // associate with transcript id
     gff_id_register(&gff->tscript_ids, aux->parent, aux->parent_end, &ftr->trid);
 
-    if ( ftr->strand==-1 && gff->verbosity > 0 )
+    if ( ftr->strand==STRAND_UNK && gff->verbosity > 0 )
     {
         if ( !gff->warned.unknown_strand || gff->verbosity > 1 )
             fprintf(bcftools_stderr,"Warning: Ignoring GFF feature with unknown strand .. %s\n",line);
         gff->warned.unknown_strand++;
     }
-    if ( ftr->phase==-1 && gff->verbosity > 0 )
+    if ( ftr->phase==CDS_PHASE_UNKN && gff->verbosity > 0 )
     {
         if ( !gff->warned.unknown_phase|| gff->verbosity > 1 )
             fprintf(bcftools_stderr,"Warning: Ignoring GFF feature with unknown phase .. %s\n",line);
@@ -509,8 +524,8 @@ static void gff_parse_gene(gff_t *gff, const char *line, ftr_t *ftr)
         gene->name = strdup(aux->gene_ids.str[gene_id]); // Name=<GeneName> field is not present, use the gene ID instead
 }
 
-// Returns 0 for exons,CDS,UTRs to indiciate these need to be pruned later and regidx built on them,
-// or -1 to indiciate the structure needs not be saved (either because of an error or because saved
+// Returns 0 for exons,CDS,UTRs to indicate these need to be pruned later and regidx built on them,
+// or -1 to indicate the structure needs not be saved (either because of an error or because saved
 // as transcript or gene.)
 static int gff_parse_line(gff_t *gff, char *line, ftr_t *ftr)
 {
@@ -556,10 +571,11 @@ static int gff_parse_line(gff_t *gff, char *line, ftr_t *ftr)
     ftr->strand = -1;
     if ( *ss == '+' ) ftr->strand = STRAND_FWD;
     else if ( *ss == '-' ) ftr->strand = STRAND_REV;
+    else ftr->strand = STRAND_UNK;
     ss += 2;
 
     // 8th column: phase (codon offset)
-    ftr->phase = -1;
+    ftr->phase = CDS_PHASE_UNKN;
     if ( *ss == '0' ) ftr->phase = 0;
     else if ( *ss == '1' ) ftr->phase = 1;
     else if ( *ss == '2' ) ftr->phase = 2;
@@ -729,12 +745,12 @@ static void tscript_init_cds(gff_t *gff)
                 if ( phase!=len%3 )
                 {
                     if ( !gff->force )
-                        error("Error: GFF3 assumption failed for transcript %s, CDS=%d: phase!=len%%3 (phase=%d, len=%d). Use the --force option to proceed anyway (at your own risk).\n",
+                        error("Error: GFF3 assumption failed for transcript %s, CDS=%"PRIu32": phase!=len%%3 (phase=%d, len=%d). Use the --force option to proceed anyway (at your own risk).\n",
                                 gff->tscript_ids.str[tr->id],tr->cds[i]->beg+1,phase,len);
                     if ( gff->verbosity > 0 )
                     {
                         if ( !gff->warned.wrong_phase || gff->verbosity > 1 )
-                            fprintf(bcftools_stderr,"Warning: The GFF has inconsistent phase column in transcript %s, skipping. CDS pos=%d: phase!=len%%3 (phase=%d, len=%d)\n",
+                            fprintf(bcftools_stderr,"Warning: The GFF has inconsistent phase column in transcript %s, skipping. CDS pos=%"PRIu32": phase!=len%%3 (phase=%d, len=%d)\n",
                                     gff->tscript_ids.str[tr->id],tr->cds[i]->beg+1,phase,len);
                         gff->warned.wrong_phase++;
                     }
@@ -745,7 +761,7 @@ static void tscript_init_cds(gff_t *gff)
             }
             if ( !tscript_ok ) continue;    // skip this transcript
         }
-        else
+        else if ( tr->strand==STRAND_REV )
         {
             if ( tr->cds[tr->ncds-1]->phase != CDS_PHASE_UNKN )
             {
@@ -792,12 +808,12 @@ static void tscript_init_cds(gff_t *gff)
                 if ( phase!=len%3 )
                 {
                     if ( !gff->force )
-                        error("Error: GFF3 assumption failed for transcript %s, CDS=%d: phase!=len%%3 (phase=%d, len=%d). Use the --force option to proceed anyway (at your own risk).\n",
+                        error("Error: GFF3 assumption failed for transcript %s, CDS=%"PRIu32": phase!=len%%3 (phase=%d, len=%d). Use the --force option to proceed anyway (at your own risk).\n",
                                 gff->tscript_ids.str[tr->id],tr->cds[i]->beg+1,phase,len);
                     if ( gff->verbosity > 0 )
                     {
                         if ( !gff->warned.wrong_phase || gff->verbosity > 1 )
-                            fprintf(bcftools_stderr,"Warning: The GFF has inconsistent phase column in transcript %s, skipping. CDS pos=%d: phase!=len%%3 (phase=%d, len=%d)\n",
+                            fprintf(bcftools_stderr,"Warning: The GFF has inconsistent phase column in transcript %s, skipping. CDS pos=%"PRIu32": phase!=len%%3 (phase=%d, len=%d)\n",
                                     gff->tscript_ids.str[tr->id],tr->cds[i]->beg+1,phase,len);
                         gff->warned.wrong_phase++;
                     }
@@ -808,6 +824,8 @@ static void tscript_init_cds(gff_t *gff)
             }
             if ( !tscript_ok ) continue;    // skip this transcript
         }
+        else
+            continue;   // unknown strand
 
         // set len. At the same check that CDS within a transcript do not overlap
         len = 0;
@@ -856,7 +874,7 @@ static void tscript_init_cds(gff_t *gff)
                     i--;
                 }
             }
-            else
+            else if ( tr->strand==STRAND_REV )
             {
                 i = 0;
                 while ( i<tr->ncds && len%3 )
@@ -898,7 +916,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         gf_gene_t *gene = (gf_gene_t*) kh_val(gff->init.gid2gene, k);
         char *gene_id = gff->init.gene_ids.str[gene->id];
         str.l = 0;
-        ksprintf(&str,"%s\t.\tgene\t%d\t%d\t.\t%c\t.\tID=%s;Name=%s;used=%d\n",gff->init.seq[gene->iseq],gene->beg+1,gene->end+1,gene->strand==STRAND_FWD?'+':'-',gene_id,gene->name,gene->used);
+        ksprintf(&str,"%s\t.\tgene\t%"PRIu32"\t%"PRIu32"\t.\t%c\t.\tID=%s;Name=%s;used=%d\n",gff->init.seq[gene->iseq],gene->beg+1,gene->end+1,gene->strand==STRAND_FWD?'+':(gene->strand==STRAND_REV?'-':'.'),gene_id,gene->name,gene->used);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
 
@@ -909,7 +927,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         char *gene_id =  gff->init.gene_ids.str[tr->gene->id];
         const char *type = tr->type==GF_PROTEIN_CODING ? "mRNA" : gf_type2gff_string(tr->type);
         str.l = 0;
-        ksprintf(&str,"%s\t.\t%s\t%d\t%d\t.\t%c\t.\tID=%s;Parent=%s;biotype=%s;used=%d\n",itr->seq,type,itr->beg+1,itr->end+1,tr->strand==STRAND_FWD?'+':'-',gff->tscript_ids.str[tr->id],gene_id,gf_type2gff_string(tr->type),tr->used);
+        ksprintf(&str,"%s\t.\t%s\t%"PRIu32"\t%"PRIu32"\t.\t%c\t.\tID=%s;Parent=%s;biotype=%s;used=%d\n",itr->seq,type,itr->beg+1,itr->end+1,tr->strand==STRAND_FWD?'+':(tr->strand==STRAND_REV?'-':'.'),gff->tscript_ids.str[tr->id],gene_id,gf_type2gff_string(tr->type),tr->used);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
     regitr_destroy(itr);
@@ -920,7 +938,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         gf_cds_t *cds = regitr_payload(itr,gf_cds_t*);
         gf_tscript_t *tr = cds->tr;
         str.l = 0;
-        ksprintf(&str,"%s\t.\tCDS\t%d\t%d\t.\t%c\t%c\tParent=%s\n",itr->seq,cds->beg+1,cds->beg+cds->len,tr->strand==STRAND_FWD?'+':'-',cds->phase==3?'.':cds->phase+(int)'0',gff->tscript_ids.str[tr->id]);
+        ksprintf(&str,"%s\t.\tCDS\t%"PRIu32"\t%"PRIu32"\t.\t%c\t%c\tParent=%s\n",itr->seq,cds->beg+1,cds->beg+cds->len,tr->strand==STRAND_FWD?'+':(tr->strand==STRAND_REV?'-':'.'),cds->phase==3?'.':cds->phase+(int)'0',gff->tscript_ids.str[tr->id]);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
     regitr_destroy(itr);
@@ -931,7 +949,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         gf_utr_t *utr = regitr_payload(itr,gf_utr_t*);
         gf_tscript_t *tr = utr->tr;
         str.l = 0;
-        ksprintf(&str,"%s\t.\t%s_prime_UTR\t%d\t%d\t.\t%c\t.\tParent=%s\n",itr->seq,utr->which==prime3?"three":"five",utr->beg+1,utr->end+1,tr->strand==STRAND_FWD?'+':'-',gff->tscript_ids.str[tr->id]);
+        ksprintf(&str,"%s\t.\t%s_prime_UTR\t%"PRIu32"\t%"PRIu32"\t.\t%c\t.\tParent=%s\n",itr->seq,utr->which==prime3?"three":"five",utr->beg+1,utr->end+1,tr->strand==STRAND_FWD?'+':(tr->strand==STRAND_REV?'-':'.'),gff->tscript_ids.str[tr->id]);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
     regitr_destroy(itr);
@@ -942,7 +960,7 @@ static int gff_dump(gff_t *gff, const char *fname)
         gf_exon_t *exon = regitr_payload(itr,gf_exon_t*);
         gf_tscript_t *tr = exon->tr;
         str.l = 0;
-        ksprintf(&str,"%s\t.\texon\t%d\t%d\t.\t%c\t.\tParent=%s\n",itr->seq,exon->beg+1,exon->end+1,tr->strand==STRAND_FWD?'+':'-',gff->tscript_ids.str[tr->id]);
+        ksprintf(&str,"%s\t.\texon\t%"PRIu32"\t%"PRIu32"\t.\t%c\t.\tParent=%s\n",itr->seq,exon->beg+1,exon->end+1,tr->strand==STRAND_FWD?'+':(tr->strand==STRAND_REV?'-':'.'),gff->tscript_ids.str[tr->id]);
         if ( bgzf_write(out, str.s, str.l) != str.l ) error("Error writing %s: %s\n", fname, strerror(errno));
     }
     regitr_destroy(itr);
@@ -1006,7 +1024,7 @@ int gff_parse(gff_t *gff)
         else if ( ftr->type==GF_UTR5 ) register_utr(gff, ftr);
         else if ( ftr->type==GF_UTR3 ) register_utr(gff, ftr);
         else
-            error("something: %s\t%d\t%d\t%s\t%s\n", aux->seq[ftr->iseq],ftr->beg+1,ftr->end+1,gff->tscript_ids.str[ftr->trid],gf_type2gff_string(ftr->type));
+            error("something: %s\t%"PRIu32"\t%"PRIu32"\t%s\t%s\n", aux->seq[ftr->iseq],ftr->beg+1,ftr->end+1,gff->tscript_ids.str[ftr->trid],gf_type2gff_string(ftr->type));
     }
     tscript_init_cds(gff);
 
@@ -1048,7 +1066,7 @@ int gff_parse(gff_t *gff)
         INC_NWARN(wrong_phase);
         INC_NWARN(overlapping_cds);
         if ( nwarn > 0 )
-            fprintf(bcftools_stderr,"Warning: %d warnings were supressed, run with `--verbose 2` to see them all\n",nwarn);
+            fprintf(bcftools_stderr,"Warning: %d warnings were suppressed, increase verbosity to see them all\n",nwarn);
     }
 
     if ( gff->dump_fname ) gff_dump(gff, gff->dump_fname);
index ebb64634a7029c5e54424ca3135110a4d96819d3..afa945e8169dd2b6953442858a3a24751569b414 100644 (file)
@@ -1,6 +1,6 @@
 /* The MIT License
 
-   Copyright (c) 2023 Genome Research Ltd.
+   Copyright (c) 2023-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -36,7 +36,7 @@
 
     Read about transcript types here
         http://vega.sanger.ac.uk/info/about/gene_and_transcript_types.html
-        http://www.ensembl.org/info/genome/variation/predicted_data.html
+        https://www.ensembl.org/info/genome/variation/prediction/predicted_data.html
         https://www.gencodegenes.org/pages/biotypes.html
 
     List of supported biotypes
 #ifndef GFF_H__
 #define GFF_H__
 
-#include <stdio.h>
-#include <stdlib.h>
-#include <assert.h>
-#include <getopt.h>
-#include <math.h>
-#include <inttypes.h>
-#include <htslib/hts.h>
-#include <htslib/khash.h>
-#include <htslib/khash_str2int.h>
-#include <htslib/kseq.h>
-#include <htslib/faidx.h>
-#include <htslib/bgzf.h>
-#include <errno.h>
-#include <unistd.h>
-#include <ctype.h>
-#include "bcftools.h"
-#include "regidx.h"
+#include <stdint.h>
 
 #ifndef __FUNCTION__
 #  define __FUNCTION__ __func__
 
 #define STRAND_REV 0
 #define STRAND_FWD 1
+#define STRAND_UNK 2
 
 #define TRIM_NONE   0
 #define TRIM_5PRIME 1
@@ -289,9 +274,9 @@ struct gf_tscript_t_
 {
     uint32_t id;        // transcript id
     uint32_t beg,end;   // transcript's beg and end coordinate (ref strand, 0-based, inclusive)
-    uint32_t strand:1,  // STRAND_REV or STRAND_FWD
+    uint32_t strand:2,  // STRAND_REV,FWD,UNK
              used:1,    // does it have any exons, UTRs, CDS?
-             ncds:30,   // number of exons
+             ncds:29,   // number of exons
              mcds;
     gf_cds_t **cds;     // ordered list of exons
     uint32_t trim:2,    // complete, 5' or 3' trimmed, see TRIM_* types
index c7b2e77d164295e1e6c264cb8bc3bd099a5c27c1..137194a44c85b41dae9f4315132e7dc358ac2a39 100644 (file)
@@ -40,7 +40,7 @@ struct _gvcf_t
 void gvcf_update_header(gvcf_t *gvcf, bcf_hdr_t *hdr)
 {
     bcf_hdr_append(hdr,"##INFO=<ID=END,Number=1,Type=Integer,Description=\"End position of the variant described in this record\">");
-    bcf_hdr_append(hdr,"##INFO=<ID=MinDP,Number=1,Type=Integer,Description=\"Minimum per-sample depth in this gVCF block\">");
+    bcf_hdr_append(hdr,"##INFO=<ID=MIN_DP,Number=1,Type=Integer,Description=\"Minimum per-sample depth in this gVCF block\">");
 }
 
 gvcf_t *gvcf_init(const char *dp_ranges)
@@ -98,7 +98,6 @@ bcf1_t *gvcf_write(gvcf_t *gvcf, htsFile *fh, bcf_hdr_t *hdr, bcf1_t *rec, int i
     // encountered, or other conditions not met (block broken by a non-ref or DP too low).
     int needs_flush = can_collapse ? 0 : 1;
 
-
     // Can the record be included in a gVCF block? That is, is this a ref-only site?
     if ( rec && can_collapse )
     {
@@ -148,7 +147,7 @@ bcf1_t *gvcf_write(gvcf_t *gvcf, htsFile *fh, bcf_hdr_t *hdr, bcf1_t *rec, int i
         bcf_update_alleles_str(hdr, gvcf->line, gvcf->als.s);
         if ( gvcf->start+1 < gvcf->end )    // create gVCF record only if it spans at least two sites
             bcf_update_info_int32(hdr, gvcf->line, "END", &gvcf->end, 1);
-        bcf_update_info_int32(hdr, gvcf->line, "MinDP", &gvcf->min_dp, 1);
+        bcf_update_info_int32(hdr, gvcf->line, "MIN_DP", &gvcf->min_dp, 1);
         if ( gvcf->nqsum>0 )
             bcf_update_info_float(hdr, gvcf->line, "QS", gvcf->qsum, gvcf->nqsum);
         if ( gvcf->ngts )
@@ -220,7 +219,7 @@ bcf1_t *gvcf_write(gvcf_t *gvcf, htsFile *fh, bcf_hdr_t *hdr, bcf1_t *rec, int i
     }
 
     if ( is_ref && min_dp )
-        bcf_update_info_int32(hdr, rec, "MinDP", &min_dp, 1);
+        bcf_update_info_int32(hdr, rec, "MIN_DP", &min_dp, 1);
 
     return rec;
 }
index bd881f4b3868235be2fe9045534214efdaf0ab83..9db3af99da39c8a255373d48ecaf65d9c022705a 100644 (file)
@@ -42,7 +42,7 @@ struct _gvcf_t
 void gvcf_update_header(gvcf_t *gvcf, bcf_hdr_t *hdr)
 {
     bcf_hdr_append(hdr,"##INFO=<ID=END,Number=1,Type=Integer,Description=\"End position of the variant described in this record\">");
-    bcf_hdr_append(hdr,"##INFO=<ID=MinDP,Number=1,Type=Integer,Description=\"Minimum per-sample depth in this gVCF block\">");
+    bcf_hdr_append(hdr,"##INFO=<ID=MIN_DP,Number=1,Type=Integer,Description=\"Minimum per-sample depth in this gVCF block\">");
 }
 
 gvcf_t *gvcf_init(const char *dp_ranges)
@@ -100,7 +100,6 @@ bcf1_t *gvcf_write(gvcf_t *gvcf, htsFile *fh, bcf_hdr_t *hdr, bcf1_t *rec, int i
     // encountered, or other conditions not met (block broken by a non-ref or DP too low).
     int needs_flush = can_collapse ? 0 : 1;
 
-
     // Can the record be included in a gVCF block? That is, is this a ref-only site?
     if ( rec && can_collapse )
     {
@@ -150,7 +149,7 @@ bcf1_t *gvcf_write(gvcf_t *gvcf, htsFile *fh, bcf_hdr_t *hdr, bcf1_t *rec, int i
         bcf_update_alleles_str(hdr, gvcf->line, gvcf->als.s);
         if ( gvcf->start+1 < gvcf->end )    // create gVCF record only if it spans at least two sites
             bcf_update_info_int32(hdr, gvcf->line, "END", &gvcf->end, 1);
-        bcf_update_info_int32(hdr, gvcf->line, "MinDP", &gvcf->min_dp, 1);
+        bcf_update_info_int32(hdr, gvcf->line, "MIN_DP", &gvcf->min_dp, 1);
         if ( gvcf->nqsum>0 )
             bcf_update_info_float(hdr, gvcf->line, "QS", gvcf->qsum, gvcf->nqsum);
         if ( gvcf->ngts )
@@ -222,7 +221,7 @@ bcf1_t *gvcf_write(gvcf_t *gvcf, htsFile *fh, bcf_hdr_t *hdr, bcf1_t *rec, int i
     }
 
     if ( is_ref && min_dp )
-        bcf_update_info_int32(hdr, rec, "MinDP", &min_dp, 1);
+        bcf_update_info_int32(hdr, rec, "MIN_DP", &min_dp, 1);
 
     return rec;
 }
index a0213589fc49e9e7952f6abbf8880300cbe334e0..14357373ecbf10b736bcd950c86651185827771b 100644 (file)
@@ -265,7 +265,7 @@ int main(int argc, char *argv[])
     if (argc < 2) { usage(stderr); return 1; }
 
     if (strcmp(argv[1], "version") == 0 || strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-v") == 0) {
-        printf("bcftools %s\nUsing htslib %s\nCopyright (C) 2023 Genome Research Ltd.\n", bcftools_version(), hts_version());
+        printf("bcftools %s\nUsing htslib %s\nCopyright (C) 2024 Genome Research Ltd.\n", bcftools_version(), hts_version());
 #if USE_GPL
         printf("License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n");
 #else
index 7608adc8bf2721671d65b1d594f1c62209aac3d1..56174fa2df57cac7336c984333657913891c4050 100644 (file)
@@ -267,7 +267,7 @@ int bcftools_main(int argc, char *argv[])
     if (argc < 2) { usage(bcftools_stderr); return 1; }
 
     if (strcmp(argv[1], "version") == 0 || strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-v") == 0) {
-        fprintf(bcftools_stdout, "bcftools %s\nUsing htslib %s\nCopyright (C) 2023 Genome Research Ltd.\n", bcftools_version(), hts_version());
+        fprintf(bcftools_stdout, "bcftools %s\nUsing htslib %s\nCopyright (C) 2024 Genome Research Ltd.\n", bcftools_version(), hts_version());
 #if USE_GPL
         fprintf(bcftools_stdout, "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n");
 #else
index 804ff013196c6f93aaaaad63fe33d8557585262e..13383787e217d29a3b13db7cc80fee467d71020c 100644 (file)
@@ -444,7 +444,7 @@ void mcall_destroy(call_t *call)
 // qual calculation is not affected.
 // Missing values are replaced by generic likelihoods when X (unseen allele) is
 // present.
-// NB: While the -m callig model uses the pdgs in canonical order,
+// NB: While the -m calling model uses the pdgs in canonical order,
 // the original samtools -c calling code uses pdgs in reverse order (AA comes
 // first, RR last).
 // NB: Ploidy is not taken into account here, which is incorrect.
@@ -1495,7 +1495,7 @@ int mcall(call_t *call, bcf1_t *rec)
     // If available, take into account reference panel AFs
     if ( call->prior_AN && bcf_get_info_int32(call->hdr, rec, call->prior_AN ,&call->ac, &call->nac)==1 )
     {
-        int an = call->ac[0];   // number of alleles total, procede only if not zero; reuse call->ac
+        int an = call->ac[0];   // number of alleles total, proceed only if not zero; reuse call->ac
         if ( an > 0 && bcf_get_info_int32(call->hdr, rec, call->prior_AC ,&call->ac, &call->nac)==nals_ori-1 )    // number of ALT alleles
         {
             int ac0 = an;       // this will become the number of REFs
@@ -1558,6 +1558,11 @@ int mcall(call_t *call, bcf1_t *rec)
     call->nals_new = 0;
     for (i=0; i<nals_ori; i++)
     {
+        if ( (call->flag&CALL_KEEP_UNSEEN) && i==unseen && call->nals_new==1 )
+        {
+            call->nals_new++;
+            call->als_new |= 1<<i;
+        }
         if ( i>0 && i==unseen ) continue;
         if ( call->flag & CALL_KEEPALT ) call->als_new |= 1<<i;
         if ( call->als_new & (1<<i) ) call->nals_new++;
@@ -1669,6 +1674,6 @@ int mcall(call_t *call, bcf1_t *rec)
 
     bcf_update_info_int32(call->hdr, rec, "I16", NULL, 0);     // remove I16 tag
 
-    return call->nals_new;
+    return is_variant ? call->nals_new : 1;
 }
 
index bf3806f5f633b97476aca1b107ef5a4c26345a36..345d1103738c39061b591cf9a34db2fa39d2db62 100644 (file)
@@ -446,7 +446,7 @@ void mcall_destroy(call_t *call)
 // qual calculation is not affected.
 // Missing values are replaced by generic likelihoods when X (unseen allele) is
 // present.
-// NB: While the -m callig model uses the pdgs in canonical order,
+// NB: While the -m calling model uses the pdgs in canonical order,
 // the original samtools -c calling code uses pdgs in reverse order (AA comes
 // first, RR last).
 // NB: Ploidy is not taken into account here, which is incorrect.
@@ -1497,7 +1497,7 @@ int mcall(call_t *call, bcf1_t *rec)
     // If available, take into account reference panel AFs
     if ( call->prior_AN && bcf_get_info_int32(call->hdr, rec, call->prior_AN ,&call->ac, &call->nac)==1 )
     {
-        int an = call->ac[0];   // number of alleles total, procede only if not zero; reuse call->ac
+        int an = call->ac[0];   // number of alleles total, proceed only if not zero; reuse call->ac
         if ( an > 0 && bcf_get_info_int32(call->hdr, rec, call->prior_AC ,&call->ac, &call->nac)==nals_ori-1 )    // number of ALT alleles
         {
             int ac0 = an;       // this will become the number of REFs
@@ -1560,6 +1560,11 @@ int mcall(call_t *call, bcf1_t *rec)
     call->nals_new = 0;
     for (i=0; i<nals_ori; i++)
     {
+        if ( (call->flag&CALL_KEEP_UNSEEN) && i==unseen && call->nals_new==1 )
+        {
+            call->nals_new++;
+            call->als_new |= 1<<i;
+        }
         if ( i>0 && i==unseen ) continue;
         if ( call->flag & CALL_KEEPALT ) call->als_new |= 1<<i;
         if ( call->als_new & (1<<i) ) call->nals_new++;
@@ -1671,6 +1676,6 @@ int mcall(call_t *call, bcf1_t *rec)
 
     bcf_update_info_int32(call->hdr, rec, "I16", NULL, 0);     // remove I16 tag
 
-    return call->nals_new;
+    return is_variant ? call->nals_new : 1;
 }
 
index d42a6a36046f5425aa96b87ff11279281f419232..943e0f6f6da1be99fc98490c4a87e9ae6b032d11 100644 (file)
@@ -1,6 +1,6 @@
 /*  mpileup.c -- mpileup subcommand. Previously bam_plcmd.c from samtools
 
-    Copyright (C) 2008-2023 Genome Research Ltd.
+    Copyright (C) 2008-2024 Genome Research Ltd.
     Portions copyright (C) 2009-2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -72,8 +72,11 @@ typedef struct {
     uint32_t fmt_flag;
     int rflag_skip_any_unset, rflag_skip_all_unset, rflag_skip_any_set, rflag_skip_all_set, output_type;
     int openQ, extQ, tandemQ, min_support, indel_win_size; // for indels
+    int seqQ_offset;
     double min_frac; // for indels
-    double indel_bias;
+    double indel_bias, poly_mqual;
+    double del_bias; // compensate for diff deletion vs insertion error rates
+    double vs_ref;
     char *reg_fname, *pl_list, *fai_fname, *output_fname;
     int reg_is_file, record_cmd_line, n_threads, clevel;
     faidx_t *fai;
@@ -99,6 +102,7 @@ typedef struct {
     htsFile *bcf_fp;
     bcf_hdr_t *bcf_hdr;
     int indels_v20;
+    int edlib;
     int argc;
     char **argv;
     int write_index;
@@ -584,12 +588,14 @@ static int mpileup_reg(mplp_conf_t *conf, uint32_t beg, uint32_t end)
 
         // call indels; todo: subsampling with total_depth>max_indel_depth instead of ignoring?
         // check me: rghash in bcf_call_gap_prep() should have no effect, reads mplp_func already excludes them
-        if ( !(conf->flag&MPLP_NO_INDEL) && total_depth < conf->max_indel_depth )
+        if (!(conf->flag&MPLP_NO_INDEL) && total_depth < conf->max_indel_depth)
         {
             bcf_callaux_clean(conf->bca, &conf->bc);
             conf->bca->chr = tid>=0 ? hdr->target_name[tid] : NULL;
             int iret;
-            if ( conf->indels_v20 )
+            if (conf->edlib)
+                iret = bcf_edlib_gap_prep(conf->gplp->n, conf->gplp->n_plp, conf->gplp->plp, pos, conf->bca, ref, ref_len);
+            else if ( conf->indels_v20 )
                 iret = bcf_iaux_gap_prep(conf->gplp->n, conf->gplp->n_plp, conf->gplp->plp, pos, conf->bca, ref);
             else
                 iret = bcf_call_gap_prep(conf->gplp->n, conf->gplp->n_plp, conf->gplp->plp, pos, conf->bca, ref);
@@ -606,7 +612,7 @@ static int mpileup_reg(mplp_conf_t *conf, uint32_t beg, uint32_t end)
             }
         }
     }
-    return 0;
+    return ret;
 }
 
 static int mpileup(mplp_conf_t *conf)
@@ -646,8 +652,12 @@ static int mpileup(mplp_conf_t *conf)
             }
         }
         nregs = regidx_nregs(conf->reg);
-        conf->reg_itr = regitr_init(conf->reg);
-        regitr_loop(conf->reg_itr);   // region iterator now positioned at the first region
+        if ( nregs )
+        {
+            // the regions list can be empty, see #2250
+            conf->reg_itr = regitr_init(conf->reg);
+            regitr_loop(conf->reg_itr);   // region iterator now positioned at the first region
+        }
     }
 
     // read the header of each file in the list and initialize data
@@ -693,7 +703,7 @@ static int mpileup(mplp_conf_t *conf)
             i--;
             continue;
         }
-        if (conf->reg) {
+        if (conf->reg && nregs) {
             hts_idx_t *idx = sam_index_load(conf->mplp_data[i]->fp, conf->files[i]);
             if (idx == NULL) {
                 fprintf(stderr, "[%s] fail to load index for %s\n", __func__, conf->files[i]);
@@ -857,13 +867,16 @@ static int mpileup(mplp_conf_t *conf)
     for (i=0; i<nsmpl; i++)
         bcf_hdr_add_sample(conf->bcf_hdr, smpl[i]);
     if ( bcf_hdr_write(conf->bcf_fp, conf->bcf_hdr)!=0 ) error("[%s] Error: failed to write the header to %s\n",__func__,conf->output_fname?conf->output_fname:"standard output");
-    if ( conf->write_index && init_index(conf->bcf_fp,conf->bcf_hdr,conf->output_fname,&conf->index_fn)<0 ) error("Error: failed to initialise index for %s\n",conf->output_fname);
+    if ( init_index2(conf->bcf_fp,conf->bcf_hdr,conf->output_fname,
+                     &conf->index_fn, conf->write_index) < 0 )
+        error("Error: failed to initialise index for %s\n",conf->output_fname);
 
     conf->bca = bcf_call_init(-1., conf->min_baseQ, conf->max_baseQ,
                               conf->delta_baseQ);
     conf->bcr = (bcf_callret1_t*) calloc(nsmpl, sizeof(bcf_callret1_t));
     conf->bca->openQ = conf->openQ, conf->bca->extQ = conf->extQ, conf->bca->tandemQ = conf->tandemQ;
     conf->bca->indel_bias = conf->indel_bias;
+    conf->bca->del_bias = conf->del_bias;
     conf->bca->min_frac = conf->min_frac;
     conf->bca->min_support = conf->min_support;
     conf->bca->per_sample_flt = conf->flag & MPLP_PER_SAMPLE;
@@ -871,6 +884,10 @@ static int mpileup(mplp_conf_t *conf)
     conf->bca->ambig_reads = conf->ambig_reads;
     conf->bca->indel_win_size = conf->indel_win_size;
     conf->bca->indels_v20 = conf->indels_v20;
+    conf->bca->edlib = conf->edlib;
+    conf->bca->seqQ_offset = conf->seqQ_offset;
+    conf->bca->poly_mqual = conf->poly_mqual;
+    conf->bca->vs_ref = conf->vs_ref;
 
     conf->bc.bcf_hdr = conf->bcf_hdr;
     conf->bc.n  = nsmpl;
@@ -925,6 +942,7 @@ static int mpileup(mplp_conf_t *conf)
 
 
     // Run mpileup for multiple regions
+    int ret = 0;
     if ( nregs )
     {
         int ireg = 0;
@@ -953,12 +971,18 @@ static int mpileup(mplp_conf_t *conf)
                     bam_mplp_reset(conf->iter);
                 }
             }
-            mpileup_reg(conf,conf->reg_itr->beg,conf->reg_itr->end);
+            ret = mpileup_reg(conf,conf->reg_itr->beg,conf->reg_itr->end);
+            if ( ret<0 ) break;
         }
         while ( regitr_loop(conf->reg_itr) );
     }
-    else
-        mpileup_reg(conf,0,UINT32_MAX);
+    else if ( !conf->reg )
+        ret = mpileup_reg(conf,0,UINT32_MAX);
+    if ( ret<0 )
+    {
+        fprintf(stderr, "[%s] failed to read from input file\n", __func__);
+        exit(EXIT_FAILURE);
+    }
 
     flush_bcf_records(conf, conf->bcf_fp, conf->bcf_hdr, NULL);
 
@@ -1150,7 +1174,7 @@ static void list_annotations(FILE *fp)
         "\n"
         "FORMAT annotation tags available (\"FORMAT/\" prefix is optional):\n"
         "\n"
-        "  FORMAT/AD   .. Allelic depth (Number=R,Type=Integer)\n"
+        "* FORMAT/AD   .. Allelic depth (Number=R,Type=Integer)\n"
         "  FORMAT/ADF  .. Allelic depths on the forward strand (Number=R,Type=Integer)\n"
         "  FORMAT/ADR  .. Allelic depths on the reverse strand (Number=R,Type=Integer)\n"
         "  FORMAT/DP   .. Number of high-quality bases (Number=1,Type=Integer)\n"
@@ -1200,7 +1224,7 @@ static void print_usage(FILE *fp, const mplp_conf_t *mplp)
         "\n"
         "Input options:\n"
         "  -6, --illumina1.3+      Quality is in the Illumina-1.3+ encoding\n"
-        "  -A, --count-orphans     Do not discard anomalous read pairs\n"
+        "  -A, --count-orphans     Include anomalous read pairs, with flag PAIRED but not PROPER_PAIR set\n"
         "  -b, --bam-list FILE     List of input BAM filenames, one per line\n"
         "  -B, --no-BAQ            Disable BAQ (per-Base Alignment Quality)\n"
         "  -C, --adjust-MQ INT     Adjust mapping quality [0]\n"
@@ -1245,10 +1269,10 @@ static void print_usage(FILE *fp, const mplp_conf_t *mplp)
         "  -O, --output-type TYPE  'b' compressed BCF; 'u' uncompressed BCF;\n"
         "                          'z' compressed VCF; 'v' uncompressed VCF; 0-9 compression level [v]\n"
         "      --threads INT       Use multithreading with INT worker threads [0]\n"
-        "      --write-index       Automatically index the output files [off]\n"
+        "  -W, --write-index[=FMT] Automatically index the output files [off]\n"
         "\n"
         "SNP/INDEL genotype likelihoods options:\n"
-        "  -X, --config STR        Specify platform specific profiles (see below)\n"
+        "  -X, --config STR        Specify platform profile (use \"-X list\" for details)\n"
         "  -e, --ext-prob INT      Phred-scaled gap extension seq error probability [%d]\n", mplp->extQ);
     fprintf(fp,
         "  -F, --gap-frac FLOAT    Minimum fraction of gapped reads [%g]\n", mplp->min_frac);
@@ -1269,24 +1293,26 @@ static void print_usage(FILE *fp, const mplp_conf_t *mplp)
         "  --ar, --ambig-reads STR   What to do with ambiguous indel reads: drop,incAD,incAD0 [drop]\n");
     fprintf(fp,
         "      --indel-bias FLOAT  Raise to favour recall over precision [%.2f]\n", mplp->indel_bias);
+    fprintf(fp,
+        "      --del-bias FLOAT    Relative likelihood of insertion to deletion [%.2f]\n", mplp->del_bias);
+    fprintf(fp,
+        "      --score-vs-ref FLOAT\n"
+        "                          Ratio of score vs ref (1) or 2nd-best allele (0) [%.2f]\n", mplp->vs_ref);
     fprintf(fp,
         "      --indel-size INT    Approximate maximum indel size considered [%d]\n", mplp->indel_win_size);
     fprintf(fp,
-        "      --indels-2.0        New EXPERIMENTAL indel calling model (diploid reference consensus)\n");
+        "      --indels-2.0        New EXPERIMENTAL indel calling model (diploid reference consensus)\n"
+        "      --indels-cns        New EXPERIMENTAL indel calling model with edlib\n"
+        "      --seqq-offset       Indel-cns tuning for indel seq-qual scores [120]\n"
+        "      --no-indels-cns     Disable CNS mode, to use after a -X profile\n"
+        "      --poly-mqual        (Edlib mode) Use minimum quality within homopolymers\n");
     fprintf(fp,"\n");
     fprintf(fp,
-        "Configuration profiles activated with -X, --config:\n"
-        "    1.12:        -Q13 -h100 -m1 -F0.002\n"
-        "    illumina:    [ default values ]\n"
-        "    ont:         -B -Q5 --max-BQ 30 -I [also try eg |bcftools call -P0.01]\n"
-        "    pacbio-ccs:  -D -Q5 --max-BQ 50 -F0.1 -o25 -e1 --delta-BQ 10 -M99999\n"
-        "\n"
-        "Notes: Assuming diploid individuals.\n"
-        "\n"
-        "Example:\n"
-        "   # See also http://samtools.github.io/bcftools/howtos/variant-calling.html\n"
-        "   bcftools mpileup -Ou -f reference.fa alignments.bam | bcftools call -mv -Ob -o calls.bcf\n"
-        "\n");
+            "Notes: Assuming diploid individuals.\n\n"
+            "Example:\n"
+            "   # See also http://samtools.github.io/bcftools/howtos/variant-calling.html\n"
+            "   bcftools mpileup -Ou -f reference.fa alignments.bam | bcftools call -mv -Ob -o calls.bcf\n"
+            "\n");
 
     free(tmp_skip_all_set);
     free(tmp_skip_any_unset);
@@ -1294,9 +1320,41 @@ static void print_usage(FILE *fp, const mplp_conf_t *mplp)
     free(tmp_skip_any_set);
 }
 
+static void print_profiles(void) {
+    printf(
+"Configuration profiles activated with -X, --config:\n\n"
+"1.12\n"
+"    -Q13 -h100 -m1 -F0.002\n\n"
+"bgi, bgi-1.20\n"
+"    --indels-cns -B --indel-size 80 -F0.1 --indel-bias 0.9 --seqq-offset 120\n\n"
+"illumina-1.18\n"
+"    --indel-size 110\n\n"
+"illumina\n"
+"illumina-1.20\n"
+"    --indels-cns --indel-size 110\n\n"
+"ont\n"
+"    -B -Q5 --max-BQ 30 -I\n\n"
+"ont-sup, ont-sup-1.20\n"
+"    --indels-cns -B -Q1 --max-BQ 35 -F0.2 -o15 -e1 -h110 --delta-BQ 99\\\n"
+"    --del-bias 0.4 --indel-bias 0.7 --poly-mqual --seqq-offset 130\\\n"
+"    --indel-size 80\n\n"
+"pacbio-ccs-1.18\n"
+"    -D -Q5 --max-BQ 50 -F0.1 -o25 -e1 --delta-BQ 10 \\\n"
+"    -M99999 --indel-size 110\n\n"
+"pacbio-ccs, pacbio-ccs-1.20\n"
+"    --indels-cns -B -Q5 --max-BQ 50 -F0.1 -o25 -e1 -h300 --delta-BQ 10 \\\n"
+"    --del-bias 0.4 --poly-mqual --indel-bias 0.9 --seqq-offset 118\\\n"
+"    --indel-size 80 --score-vs-ref 0.7\n\n"
+"ultima, ultima-1.20\n"
+"    --indels-cns -B -Q1 --max-BQ 30 -F0.15 -o20 -e10 -h250 --delta-BQ 10 \\\n"
+"    --del-bias 0.3 --indel-bias 0.7 --poly-mqual --seqq-offset 140 \\\n"
+"    --indel-size 80 --score-vs-ref 0.3\n\n"
+"\n");
+}
+
 int main_mpileup(int argc, char *argv[])
 {
-    int c;
+    int c, i, ret = 1;
     const char *file_list = NULL;
     char **fn = NULL;
     int nfiles = 0, use_orphan = 0, noref = 0;
@@ -1309,6 +1367,7 @@ int main_mpileup(int argc, char *argv[])
     mplp.max_depth = 250; mplp.max_indel_depth = 250;
     mplp.openQ = 40; mplp.extQ = 20; mplp.tandemQ = 500;
     mplp.min_frac = 0.05; mplp.indel_bias = 1.0; mplp.min_support = 2;
+    mplp.vs_ref = 0;
     mplp.flag = MPLP_NO_ORPHAN | MPLP_REALN | MPLP_REALN_PARTIAL
               | MPLP_SMART_OVERLAPS;
     mplp.argc = argc; mplp.argv = argv;
@@ -1319,11 +1378,14 @@ int main_mpileup(int argc, char *argv[])
     mplp.n_threads = 0;
     mplp.bsmpl = bam_smpl_init();
     // the default to be changed in future, see also parse_format_flag()
-    mplp.fmt_flag = B2B_INFO_BQBZ|B2B_INFO_IDV|B2B_INFO_IMF|B2B_INFO_MQ0F|B2B_INFO_MQBZ|B2B_INFO_MQSBZ|B2B_INFO_RPBZ|B2B_INFO_SCBZ|B2B_INFO_SGB|B2B_INFO_VDB;
+    mplp.fmt_flag = B2B_INFO_BQBZ|B2B_INFO_IDV|B2B_INFO_IMF|B2B_INFO_MQ0F|B2B_INFO_MQBZ|B2B_INFO_MQSBZ|B2B_INFO_RPBZ|B2B_INFO_SCBZ|B2B_INFO_SGB|B2B_INFO_VDB|B2B_FMT_AD;
     mplp.max_read_len = 500;
     mplp.ambig_reads = B2B_DROP;
     mplp.indel_win_size = 110;
+    mplp.poly_mqual = 0;
+    mplp.seqQ_offset = 120;
     mplp.clevel = -1;
+    mplp.del_bias = 0; // even insertion and deletion likelhoods.
     hts_srand48(0);
 
     static const struct option lopts[] =
@@ -1382,6 +1444,8 @@ int main_mpileup(int argc, char *argv[])
         {"indel-bias", required_argument, NULL, 10},
         {"indel-size", required_argument, NULL, 15},
         {"indels-2.0", no_argument, NULL, 20},
+        {"indels-cns", no_argument, NULL, 22},
+        {"no-indels-cns", no_argument, NULL, 25},
         {"tandem-qual", required_argument, NULL, 'h'},
         {"skip-indels", no_argument, NULL, 'I'},
         {"max-idepth", required_argument, NULL, 'L'},
@@ -1394,27 +1458,44 @@ int main_mpileup(int argc, char *argv[])
         {"seed", required_argument, NULL, 13},
         {"ambig-reads", required_argument, NULL, 14},
         {"ar", required_argument, NULL, 14},
-        {"write-index",no_argument,NULL,21},
+        {"write-index",optional_argument,NULL,'W'},
+        {"del-bias", required_argument, NULL, 23},
+        {"poly-mqual", no_argument, NULL, 24},
+        {"no-poly-mqual", no_argument, NULL, 26},
+        {"score-vs-ref",required_argument, NULL, 27},
+        {"seqq-offset", required_argument, NULL, 28},
         {NULL, 0, NULL, 0}
     };
-    while ((c = getopt_long(argc, argv, "Ag:f:r:R:q:Q:C:BDd:L:b:P:po:e:h:Im:F:EG:6O:xa:s:S:t:T:M:X:U",lopts,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "Ag:f:r:R:q:Q:C:BDd:L:b:P:po:e:h:Im:F:EG:6O:xa:s:S:t:T:M:X:UW::",lopts,NULL)) >= 0) {
         switch (c) {
         case 'x': mplp.flag &= ~MPLP_SMART_OVERLAPS; break;
         case  16 :
             mplp.rflag_skip_any_unset = bam_str2flag(optarg);
-            if ( mplp.rflag_skip_any_unset <0 ) { fprintf(stderr,"Could not parse --nf %s\n", optarg); return 1; }
+            if ( mplp.rflag_skip_any_unset <0 ) {
+                fprintf(stderr,"Could not parse --nf %s\n", optarg);
+                goto err;
+            }
             break;
         case  17 :
             mplp.rflag_skip_all_unset = bam_str2flag(optarg);
-            if ( mplp.rflag_skip_all_unset<0 ) { fprintf(stderr,"Could not parse --if %s\n", optarg); return 1; }
+            if ( mplp.rflag_skip_all_unset<0 ) {
+                fprintf(stderr,"Could not parse --if %s\n", optarg);
+                goto err;
+            }
             break;
         case  18 :
             mplp.rflag_skip_any_set = bam_str2flag(optarg);
-            if ( mplp.rflag_skip_any_set <0 ) { fprintf(stderr,"Could not parse --ef %s\n", optarg); return 1; }
+            if ( mplp.rflag_skip_any_set <0 ) {
+                fprintf(stderr,"Could not parse --ef %s\n", optarg);
+                goto err;
+            }
             break;
         case  19 :
             mplp.rflag_skip_all_set = bam_str2flag(optarg);
-            if ( mplp.rflag_skip_all_set <0 ) { fprintf(stderr,"Could not parse --df %s\n", optarg); return 1; }
+            if ( mplp.rflag_skip_all_set <0 ) {
+                fprintf(stderr,"Could not parse --df %s\n", optarg);
+                goto err;
+            }
             break;
         case  3 : mplp.output_fname = optarg; break;
         case  4 : mplp.openQ = atoi(optarg); break;
@@ -1425,7 +1506,8 @@ int main_mpileup(int argc, char *argv[])
             break;
         case 'f':
             mplp.fai = fai_load(optarg);
-            if (mplp.fai == NULL) return 1;
+            if (mplp.fai == NULL)
+                goto err;
             mplp.fai_fname = optarg;
             break;
         case  7 : noref = 1; break;
@@ -1452,7 +1534,10 @@ int main_mpileup(int argc, char *argv[])
                   if ( optarg[0]=='^' ) optarg++;
                   else mplp.bed_logic = 1;
                   mplp.bed = regidx_init(optarg,NULL,NULL,0,NULL);
-                  if (!mplp.bed) { fprintf(stderr, "bcftools mpileup: Could not read file \"%s\"", optarg); return 1; }
+                  if (!mplp.bed) {
+                      fprintf(stderr, "bcftools mpileup: Could not read file \"%s\"", optarg);
+                      goto err;
+                  }
                   break;
         case 'P': mplp.pl_list = strdup(optarg); break;
         case 'p': mplp.flag |= MPLP_PER_SAMPLE; break;
@@ -1505,19 +1590,39 @@ int main_mpileup(int argc, char *argv[])
             else
                 mplp.indel_bias = 1/atof(optarg);
             break;
+        case 27:
+            mplp.vs_ref = atof(optarg);
+            //if (mplp.vs_ref < 0) mplp.vs_ref = 0;
+            if (mplp.vs_ref > 1) mplp.vs_ref = 1;
+            break;
         case  15: {
                 char *tmp;
                 mplp.indel_win_size = strtol(optarg,&tmp,10);
                 if ( *tmp ) error("Could not parse argument: --indel-size %s\n", optarg);
-                if ( mplp.indel_win_size < 110 )
+                if ( mplp.indel_win_size < 20 )
                 {
-                    mplp.indel_win_size = 110;
+                    mplp.indel_win_size = 20;
                     fprintf(stderr,"Warning: running with --indel-size %d, the requested value is too small\n",mplp.indel_win_size);
                 }
             }
             break;
-        case  20: mplp.indels_v20 = 1; break;
-        case  21: mplp.write_index = 1; break;
+        case  20: mplp.indels_v20 = 1; mplp.edlib = 0; break;
+        case 'W':
+            if (!(mplp.write_index = write_index_parse(optarg)))
+                error("Unsupported index format '%s'\n", optarg);
+            break;
+        case  22: mplp.edlib = 1; mplp.indels_v20 = 0; break;
+        case  25: mplp.edlib = 0; break;
+        case  28:
+            mplp.seqQ_offset = atoi(optarg);
+            if (mplp.seqQ_offset < 100)
+                mplp.seqQ_offset = 100;
+            if (mplp.seqQ_offset > 200)
+                mplp.seqQ_offset = 200;
+            break;
+        case  23: mplp.del_bias = atof(optarg); break;
+        case  24: mplp.poly_mqual = 1; break;
+        case  26: mplp.poly_mqual = 0; break;
         case 'A': use_orphan = 1; break;
         case 'F': mplp.min_frac = atof(optarg); break;
         case 'm': mplp.min_support = atoi(optarg); break;
@@ -1526,13 +1631,13 @@ int main_mpileup(int argc, char *argv[])
         case 'a':
             if (optarg[0]=='?') {
                 list_annotations(stderr);
-                return 1;
+                goto err;
             }
             parse_format_flag(&mplp.fmt_flag,optarg);
         break;
         case 'M': mplp.max_read_len = atoi(optarg); break;
         case 'X':
-            if (strcasecmp(optarg, "pacbio-ccs") == 0) {
+            if (strcasecmp(optarg, "pacbio-ccs-1.18") == 0) {
                 mplp.min_frac = 0.1;
                 mplp.min_baseQ = 5;
                 mplp.max_baseQ = 50;
@@ -1541,13 +1646,70 @@ int main_mpileup(int argc, char *argv[])
                 mplp.extQ = 1;
                 mplp.flag |= MPLP_REALN_PARTIAL;
                 mplp.max_read_len = 99999;
+
+            } else if (strcasecmp(optarg, "pacbio-ccs") == 0 ||
+                strcasecmp(optarg, "pacbio-ccs-1.20") == 0) {
+                mplp.min_frac = 0.1;
+                mplp.min_baseQ = 5;
+                mplp.max_baseQ = 50;
+                mplp.delta_baseQ = 10;
+                mplp.tandemQ = 300;
+                mplp.openQ = 25;
+                mplp.extQ = 1;
+                mplp.flag &= ~MPLP_REALN;
+                mplp.del_bias = 0.4;
+                mplp.indel_bias = 1/.9;
+                mplp.seqQ_offset = 118;
+                mplp.poly_mqual = 1;
+                mplp.edlib = 1;
+                mplp.vs_ref = 0.7;
+                mplp.indel_win_size = 80;
+
             } else if (strcasecmp(optarg, "ont") == 0) {
-                fprintf(stderr, "For ONT it may be beneficial to also run bcftools call with "
+                fprintf(stderr, "With old ONT data may be beneficial to also run bcftools call with "
                         "a higher -P, eg -P0.01 or -P 0.1\n");
                 mplp.min_baseQ = 5;
                 mplp.max_baseQ = 30;
                 mplp.flag &= ~MPLP_REALN;
                 mplp.flag |= MPLP_NO_INDEL;
+
+            } else if (strcasecmp(optarg, "ont-sup") == 0 ||
+                       strcasecmp(optarg, "ont-sup-1.20") == 0) {
+                mplp.min_frac = 0.2;
+                mplp.min_baseQ = 1;
+                mplp.max_baseQ = 35;
+                mplp.delta_baseQ = 99;
+                mplp.openQ = 15;
+                mplp.extQ = 1;
+                mplp.flag &= ~MPLP_REALN;
+                mplp.max_read_len = 9999999;
+                mplp.del_bias = 0.4;
+                mplp.poly_mqual = 1;
+                mplp.edlib = 1;
+                // If we increase -h then we can increase bias denominator too
+                mplp.tandemQ = 110;
+                mplp.indel_bias = 1/0.7;
+                mplp.seqQ_offset = 130;
+                mplp.indel_win_size = 80;
+
+            } else if (strcasecmp(optarg, "ultima") == 0 ||
+                       strcasecmp(optarg, "ultima-1.20") == 0) {
+                mplp.min_frac = 0.15;
+                mplp.min_baseQ = 1;
+                mplp.max_baseQ = 30;
+                mplp.delta_baseQ = 10;
+                mplp.openQ = 20;
+                mplp.extQ = 10;
+                mplp.tandemQ = 250;
+                mplp.flag &= ~MPLP_REALN;
+                mplp.del_bias = 0.3;
+                mplp.poly_mqual = 1;
+                mplp.edlib = 1;
+                mplp.indel_bias = 1/0.7;
+                mplp.seqQ_offset = 140;
+                mplp.vs_ref = 0.3;
+                mplp.indel_win_size = 80;
+
             } else if (strcasecmp(optarg, "1.12") == 0) {
                 // 1.12 and earlier
                 mplp.min_frac = 0.002;
@@ -1556,13 +1718,38 @@ int main_mpileup(int argc, char *argv[])
                 mplp.tandemQ = 100;
                 mplp.flag &= ~MPLP_REALN_PARTIAL;
                 mplp.flag |= MPLP_REALN;
-            } else if (strcasecmp(optarg, "illumina") == 0) {
+
+            } else if (strcasecmp(optarg, "illumina-1.18") == 0) {
+                mplp.indel_win_size = 110;
+                mplp.flag |= MPLP_REALN_PARTIAL;
+
+            } else if (strcasecmp(optarg, "illumina") == 0 ||
+                       strcasecmp(optarg, "illumina-1.20") == 0) {
+                mplp.edlib = 1;
+                mplp.indel_win_size = 110;
                 mplp.flag |= MPLP_REALN_PARTIAL;
+                mplp.indel_bias = 1;
+                mplp.seqQ_offset = 125;
+                //mplp.indel_win_size = 80; TEST?
+
+            } else if (strcasecmp(optarg, "bgi") == 0 ||
+                       strcasecmp(optarg, "bgi-1.20") == 0) {
+                mplp.min_frac = 0.1;
+                mplp.edlib = 1;
+                mplp.indel_bias = 1;
+                mplp.seqQ_offset = 120;
+                mplp.flag |= MPLP_REALN_PARTIAL;
+                mplp.indel_win_size = 80;
+
+            } else if (strcasecmp(optarg, "list") == 0 ||
+                       strcasecmp(optarg, "help") == 0) {
+                print_profiles();
+                goto err;
             } else {
                 fprintf(stderr, "Unknown configuration name '%s'\n"
-                        "Please choose from 1.12, illumina, pacbio-ccs or ont\n",
+                        "Please use '-X list' to show available choices.\n",
                         optarg);
-                return 1;
+                goto err;
             }
             break;
         case 13: hts_srand48(atoi(optarg)); break;
@@ -1574,7 +1761,7 @@ int main_mpileup(int argc, char *argv[])
             break;
         default:
             fprintf(stderr,"Invalid option: '%c'\n", c);
-            return 1;
+            goto err;
         }
     }
 
@@ -1599,22 +1786,23 @@ int main_mpileup(int argc, char *argv[])
     if ( !(mplp.flag&MPLP_REALN) && mplp.flag&MPLP_REDO_BAQ )
     {
         fprintf(stderr,"Error: The -B option cannot be combined with -E\n");
-        return 1;
+        goto err;
     }
     if (use_orphan) mplp.flag &= ~MPLP_NO_ORPHAN;
     if (argc == 1)
     {
         print_usage(stderr, &mplp);
-        return 1;
+        goto err;
     }
     if (!mplp.fai && !noref) {
         fprintf(stderr,"Error: mpileup requires the --fasta-ref option by default; use --no-reference to run without a fasta reference\n");
-        return 1;
+        goto err;
     }
-    int ret,i;
+
     if (file_list)
     {
-        if ( read_file_list(file_list,&nfiles,&fn) ) return 1;
+        if ( read_file_list(file_list,&nfiles,&fn) )
+            goto err;
         mplp.files  = fn;
         mplp.nfiles = nfiles;
     }
@@ -1633,6 +1821,8 @@ int main_mpileup(int argc, char *argv[])
     if (mplp.bed) regidx_destroy(mplp.bed);
     if (mplp.bed_itr) regitr_destroy(mplp.bed_itr);
     if (mplp.reg) regidx_destroy(mplp.reg);
+
+ err:
     bam_smpl_destroy(mplp.bsmpl);
 
     return ret;
index 81c5849c55fa88cc3ed4bdbd79c57c8389e20574..4458b60f39f78594b5da6f61e3b16dce0bae1bf2 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  mpileup.c -- mpileup subcommand. Previously bam_plcmd.c from samtools
 
-    Copyright (C) 2008-2023 Genome Research Ltd.
+    Copyright (C) 2008-2024 Genome Research Ltd.
     Portions copyright (C) 2009-2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -74,8 +74,11 @@ typedef struct {
     uint32_t fmt_flag;
     int rflag_skip_any_unset, rflag_skip_all_unset, rflag_skip_any_set, rflag_skip_all_set, output_type;
     int openQ, extQ, tandemQ, min_support, indel_win_size; // for indels
+    int seqQ_offset;
     double min_frac; // for indels
-    double indel_bias;
+    double indel_bias, poly_mqual;
+    double del_bias; // compensate for diff deletion vs insertion error rates
+    double vs_ref;
     char *reg_fname, *pl_list, *fai_fname, *output_fname;
     int reg_is_file, record_cmd_line, n_threads, clevel;
     faidx_t *fai;
@@ -101,6 +104,7 @@ typedef struct {
     htsFile *bcf_fp;
     bcf_hdr_t *bcf_hdr;
     int indels_v20;
+    int edlib;
     int argc;
     char **argv;
     int write_index;
@@ -586,12 +590,14 @@ static int mpileup_reg(mplp_conf_t *conf, uint32_t beg, uint32_t end)
 
         // call indels; todo: subsampling with total_depth>max_indel_depth instead of ignoring?
         // check me: rghash in bcf_call_gap_prep() should have no effect, reads mplp_func already excludes them
-        if ( !(conf->flag&MPLP_NO_INDEL) && total_depth < conf->max_indel_depth )
+        if (!(conf->flag&MPLP_NO_INDEL) && total_depth < conf->max_indel_depth)
         {
             bcf_callaux_clean(conf->bca, &conf->bc);
             conf->bca->chr = tid>=0 ? hdr->target_name[tid] : NULL;
             int iret;
-            if ( conf->indels_v20 )
+            if (conf->edlib)
+                iret = bcf_edlib_gap_prep(conf->gplp->n, conf->gplp->n_plp, conf->gplp->plp, pos, conf->bca, ref, ref_len);
+            else if ( conf->indels_v20 )
                 iret = bcf_iaux_gap_prep(conf->gplp->n, conf->gplp->n_plp, conf->gplp->plp, pos, conf->bca, ref);
             else
                 iret = bcf_call_gap_prep(conf->gplp->n, conf->gplp->n_plp, conf->gplp->plp, pos, conf->bca, ref);
@@ -608,7 +614,7 @@ static int mpileup_reg(mplp_conf_t *conf, uint32_t beg, uint32_t end)
             }
         }
     }
-    return 0;
+    return ret;
 }
 
 static int mpileup(mplp_conf_t *conf)
@@ -648,8 +654,12 @@ static int mpileup(mplp_conf_t *conf)
             }
         }
         nregs = regidx_nregs(conf->reg);
-        conf->reg_itr = regitr_init(conf->reg);
-        regitr_loop(conf->reg_itr);   // region iterator now positioned at the first region
+        if ( nregs )
+        {
+            // the regions list can be empty, see #2250
+            conf->reg_itr = regitr_init(conf->reg);
+            regitr_loop(conf->reg_itr);   // region iterator now positioned at the first region
+        }
     }
 
     // read the header of each file in the list and initialize data
@@ -695,7 +705,7 @@ static int mpileup(mplp_conf_t *conf)
             i--;
             continue;
         }
-        if (conf->reg) {
+        if (conf->reg && nregs) {
             hts_idx_t *idx = sam_index_load(conf->mplp_data[i]->fp, conf->files[i]);
             if (idx == NULL) {
                 fprintf(bcftools_stderr, "[%s] fail to load index for %s\n", __func__, conf->files[i]);
@@ -859,13 +869,16 @@ static int mpileup(mplp_conf_t *conf)
     for (i=0; i<nsmpl; i++)
         bcf_hdr_add_sample(conf->bcf_hdr, smpl[i]);
     if ( bcf_hdr_write(conf->bcf_fp, conf->bcf_hdr)!=0 ) error("[%s] Error: failed to write the header to %s\n",__func__,conf->output_fname?conf->output_fname:"standard output");
-    if ( conf->write_index && init_index(conf->bcf_fp,conf->bcf_hdr,conf->output_fname,&conf->index_fn)<0 ) error("Error: failed to initialise index for %s\n",conf->output_fname);
+    if ( init_index2(conf->bcf_fp,conf->bcf_hdr,conf->output_fname,
+                     &conf->index_fn, conf->write_index) < 0 )
+        error("Error: failed to initialise index for %s\n",conf->output_fname);
 
     conf->bca = bcf_call_init(-1., conf->min_baseQ, conf->max_baseQ,
                               conf->delta_baseQ);
     conf->bcr = (bcf_callret1_t*) calloc(nsmpl, sizeof(bcf_callret1_t));
     conf->bca->openQ = conf->openQ, conf->bca->extQ = conf->extQ, conf->bca->tandemQ = conf->tandemQ;
     conf->bca->indel_bias = conf->indel_bias;
+    conf->bca->del_bias = conf->del_bias;
     conf->bca->min_frac = conf->min_frac;
     conf->bca->min_support = conf->min_support;
     conf->bca->per_sample_flt = conf->flag & MPLP_PER_SAMPLE;
@@ -873,6 +886,10 @@ static int mpileup(mplp_conf_t *conf)
     conf->bca->ambig_reads = conf->ambig_reads;
     conf->bca->indel_win_size = conf->indel_win_size;
     conf->bca->indels_v20 = conf->indels_v20;
+    conf->bca->edlib = conf->edlib;
+    conf->bca->seqQ_offset = conf->seqQ_offset;
+    conf->bca->poly_mqual = conf->poly_mqual;
+    conf->bca->vs_ref = conf->vs_ref;
 
     conf->bc.bcf_hdr = conf->bcf_hdr;
     conf->bc.n  = nsmpl;
@@ -927,6 +944,7 @@ static int mpileup(mplp_conf_t *conf)
 
 
     // Run mpileup for multiple regions
+    int ret = 0;
     if ( nregs )
     {
         int ireg = 0;
@@ -955,12 +973,18 @@ static int mpileup(mplp_conf_t *conf)
                     bam_mplp_reset(conf->iter);
                 }
             }
-            mpileup_reg(conf,conf->reg_itr->beg,conf->reg_itr->end);
+            ret = mpileup_reg(conf,conf->reg_itr->beg,conf->reg_itr->end);
+            if ( ret<0 ) break;
         }
         while ( regitr_loop(conf->reg_itr) );
     }
-    else
-        mpileup_reg(conf,0,UINT32_MAX);
+    else if ( !conf->reg )
+        ret = mpileup_reg(conf,0,UINT32_MAX);
+    if ( ret<0 )
+    {
+        fprintf(bcftools_stderr, "[%s] failed to read from input file\n", __func__);
+        bcftools_exit(EXIT_FAILURE);
+    }
 
     flush_bcf_records(conf, conf->bcf_fp, conf->bcf_hdr, NULL);
 
@@ -1152,7 +1176,7 @@ static void list_annotations(FILE *fp)
         "\n"
         "FORMAT annotation tags available (\"FORMAT/\" prefix is optional):\n"
         "\n"
-        "  FORMAT/AD   .. Allelic depth (Number=R,Type=Integer)\n"
+        "* FORMAT/AD   .. Allelic depth (Number=R,Type=Integer)\n"
         "  FORMAT/ADF  .. Allelic depths on the forward strand (Number=R,Type=Integer)\n"
         "  FORMAT/ADR  .. Allelic depths on the reverse strand (Number=R,Type=Integer)\n"
         "  FORMAT/DP   .. Number of high-quality bases (Number=1,Type=Integer)\n"
@@ -1202,7 +1226,7 @@ static void print_usage(FILE *fp, const mplp_conf_t *mplp)
         "\n"
         "Input options:\n"
         "  -6, --illumina1.3+      Quality is in the Illumina-1.3+ encoding\n"
-        "  -A, --count-orphans     Do not discard anomalous read pairs\n"
+        "  -A, --count-orphans     Include anomalous read pairs, with flag PAIRED but not PROPER_PAIR set\n"
         "  -b, --bam-list FILE     List of input BAM filenames, one per line\n"
         "  -B, --no-BAQ            Disable BAQ (per-Base Alignment Quality)\n"
         "  -C, --adjust-MQ INT     Adjust mapping quality [0]\n"
@@ -1247,10 +1271,10 @@ static void print_usage(FILE *fp, const mplp_conf_t *mplp)
         "  -O, --output-type TYPE  'b' compressed BCF; 'u' uncompressed BCF;\n"
         "                          'z' compressed VCF; 'v' uncompressed VCF; 0-9 compression level [v]\n"
         "      --threads INT       Use multithreading with INT worker threads [0]\n"
-        "      --write-index       Automatically index the output files [off]\n"
+        "  -W, --write-index[=FMT] Automatically index the output files [off]\n"
         "\n"
         "SNP/INDEL genotype likelihoods options:\n"
-        "  -X, --config STR        Specify platform specific profiles (see below)\n"
+        "  -X, --config STR        Specify platform profile (use \"-X list\" for details)\n"
         "  -e, --ext-prob INT      Phred-scaled gap extension seq error probability [%d]\n", mplp->extQ);
     fprintf(fp,
         "  -F, --gap-frac FLOAT    Minimum fraction of gapped reads [%g]\n", mplp->min_frac);
@@ -1271,24 +1295,26 @@ static void print_usage(FILE *fp, const mplp_conf_t *mplp)
         "  --ar, --ambig-reads STR   What to do with ambiguous indel reads: drop,incAD,incAD0 [drop]\n");
     fprintf(fp,
         "      --indel-bias FLOAT  Raise to favour recall over precision [%.2f]\n", mplp->indel_bias);
+    fprintf(fp,
+        "      --del-bias FLOAT    Relative likelihood of insertion to deletion [%.2f]\n", mplp->del_bias);
+    fprintf(fp,
+        "      --score-vs-ref FLOAT\n"
+        "                          Ratio of score vs ref (1) or 2nd-best allele (0) [%.2f]\n", mplp->vs_ref);
     fprintf(fp,
         "      --indel-size INT    Approximate maximum indel size considered [%d]\n", mplp->indel_win_size);
     fprintf(fp,
-        "      --indels-2.0        New EXPERIMENTAL indel calling model (diploid reference consensus)\n");
+        "      --indels-2.0        New EXPERIMENTAL indel calling model (diploid reference consensus)\n"
+        "      --indels-cns        New EXPERIMENTAL indel calling model with edlib\n"
+        "      --seqq-offset       Indel-cns tuning for indel seq-qual scores [120]\n"
+        "      --no-indels-cns     Disable CNS mode, to use after a -X profile\n"
+        "      --poly-mqual        (Edlib mode) Use minimum quality within homopolymers\n");
     fprintf(fp,"\n");
     fprintf(fp,
-        "Configuration profiles activated with -X, --config:\n"
-        "    1.12:        -Q13 -h100 -m1 -F0.002\n"
-        "    illumina:    [ default values ]\n"
-        "    ont:         -B -Q5 --max-BQ 30 -I [also try eg |bcftools call -P0.01]\n"
-        "    pacbio-ccs:  -D -Q5 --max-BQ 50 -F0.1 -o25 -e1 --delta-BQ 10 -M99999\n"
-        "\n"
-        "Notes: Assuming diploid individuals.\n"
-        "\n"
-        "Example:\n"
-        "   # See also http://samtools.github.io/bcftools/howtos/variant-calling.html\n"
-        "   bcftools mpileup -Ou -f reference.fa alignments.bam | bcftools call -mv -Ob -o calls.bcf\n"
-        "\n");
+            "Notes: Assuming diploid individuals.\n\n"
+            "Example:\n"
+            "   # See also http://samtools.github.io/bcftools/howtos/variant-calling.html\n"
+            "   bcftools mpileup -Ou -f reference.fa alignments.bam | bcftools call -mv -Ob -o calls.bcf\n"
+            "\n");
 
     free(tmp_skip_all_set);
     free(tmp_skip_any_unset);
@@ -1296,9 +1322,41 @@ static void print_usage(FILE *fp, const mplp_conf_t *mplp)
     free(tmp_skip_any_set);
 }
 
+static void print_profiles(void) {
+    fprintf(bcftools_stdout, 
+"Configuration profiles activated with -X, --config:\n\n"
+"1.12\n"
+"    -Q13 -h100 -m1 -F0.002\n\n"
+"bgi, bgi-1.20\n"
+"    --indels-cns -B --indel-size 80 -F0.1 --indel-bias 0.9 --seqq-offset 120\n\n"
+"illumina-1.18\n"
+"    --indel-size 110\n\n"
+"illumina\n"
+"illumina-1.20\n"
+"    --indels-cns --indel-size 110\n\n"
+"ont\n"
+"    -B -Q5 --max-BQ 30 -I\n\n"
+"ont-sup, ont-sup-1.20\n"
+"    --indels-cns -B -Q1 --max-BQ 35 -F0.2 -o15 -e1 -h110 --delta-BQ 99\\\n"
+"    --del-bias 0.4 --indel-bias 0.7 --poly-mqual --seqq-offset 130\\\n"
+"    --indel-size 80\n\n"
+"pacbio-ccs-1.18\n"
+"    -D -Q5 --max-BQ 50 -F0.1 -o25 -e1 --delta-BQ 10 \\\n"
+"    -M99999 --indel-size 110\n\n"
+"pacbio-ccs, pacbio-ccs-1.20\n"
+"    --indels-cns -B -Q5 --max-BQ 50 -F0.1 -o25 -e1 -h300 --delta-BQ 10 \\\n"
+"    --del-bias 0.4 --poly-mqual --indel-bias 0.9 --seqq-offset 118\\\n"
+"    --indel-size 80 --score-vs-ref 0.7\n\n"
+"ultima, ultima-1.20\n"
+"    --indels-cns -B -Q1 --max-BQ 30 -F0.15 -o20 -e10 -h250 --delta-BQ 10 \\\n"
+"    --del-bias 0.3 --indel-bias 0.7 --poly-mqual --seqq-offset 140 \\\n"
+"    --indel-size 80 --score-vs-ref 0.3\n\n"
+"\n");
+}
+
 int main_mpileup(int argc, char *argv[])
 {
-    int c;
+    int c, i, ret = 1;
     const char *file_list = NULL;
     char **fn = NULL;
     int nfiles = 0, use_orphan = 0, noref = 0;
@@ -1311,6 +1369,7 @@ int main_mpileup(int argc, char *argv[])
     mplp.max_depth = 250; mplp.max_indel_depth = 250;
     mplp.openQ = 40; mplp.extQ = 20; mplp.tandemQ = 500;
     mplp.min_frac = 0.05; mplp.indel_bias = 1.0; mplp.min_support = 2;
+    mplp.vs_ref = 0;
     mplp.flag = MPLP_NO_ORPHAN | MPLP_REALN | MPLP_REALN_PARTIAL
               | MPLP_SMART_OVERLAPS;
     mplp.argc = argc; mplp.argv = argv;
@@ -1321,11 +1380,14 @@ int main_mpileup(int argc, char *argv[])
     mplp.n_threads = 0;
     mplp.bsmpl = bam_smpl_init();
     // the default to be changed in future, see also parse_format_flag()
-    mplp.fmt_flag = B2B_INFO_BQBZ|B2B_INFO_IDV|B2B_INFO_IMF|B2B_INFO_MQ0F|B2B_INFO_MQBZ|B2B_INFO_MQSBZ|B2B_INFO_RPBZ|B2B_INFO_SCBZ|B2B_INFO_SGB|B2B_INFO_VDB;
+    mplp.fmt_flag = B2B_INFO_BQBZ|B2B_INFO_IDV|B2B_INFO_IMF|B2B_INFO_MQ0F|B2B_INFO_MQBZ|B2B_INFO_MQSBZ|B2B_INFO_RPBZ|B2B_INFO_SCBZ|B2B_INFO_SGB|B2B_INFO_VDB|B2B_FMT_AD;
     mplp.max_read_len = 500;
     mplp.ambig_reads = B2B_DROP;
     mplp.indel_win_size = 110;
+    mplp.poly_mqual = 0;
+    mplp.seqQ_offset = 120;
     mplp.clevel = -1;
+    mplp.del_bias = 0; // even insertion and deletion likelhoods.
     hts_srand48(0);
 
     static const struct option lopts[] =
@@ -1384,6 +1446,8 @@ int main_mpileup(int argc, char *argv[])
         {"indel-bias", required_argument, NULL, 10},
         {"indel-size", required_argument, NULL, 15},
         {"indels-2.0", no_argument, NULL, 20},
+        {"indels-cns", no_argument, NULL, 22},
+        {"no-indels-cns", no_argument, NULL, 25},
         {"tandem-qual", required_argument, NULL, 'h'},
         {"skip-indels", no_argument, NULL, 'I'},
         {"max-idepth", required_argument, NULL, 'L'},
@@ -1396,27 +1460,44 @@ int main_mpileup(int argc, char *argv[])
         {"seed", required_argument, NULL, 13},
         {"ambig-reads", required_argument, NULL, 14},
         {"ar", required_argument, NULL, 14},
-        {"write-index",no_argument,NULL,21},
+        {"write-index",optional_argument,NULL,'W'},
+        {"del-bias", required_argument, NULL, 23},
+        {"poly-mqual", no_argument, NULL, 24},
+        {"no-poly-mqual", no_argument, NULL, 26},
+        {"score-vs-ref",required_argument, NULL, 27},
+        {"seqq-offset", required_argument, NULL, 28},
         {NULL, 0, NULL, 0}
     };
-    while ((c = getopt_long(argc, argv, "Ag:f:r:R:q:Q:C:BDd:L:b:P:po:e:h:Im:F:EG:6O:xa:s:S:t:T:M:X:U",lopts,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "Ag:f:r:R:q:Q:C:BDd:L:b:P:po:e:h:Im:F:EG:6O:xa:s:S:t:T:M:X:UW::",lopts,NULL)) >= 0) {
         switch (c) {
         case 'x': mplp.flag &= ~MPLP_SMART_OVERLAPS; break;
         case  16 :
             mplp.rflag_skip_any_unset = bam_str2flag(optarg);
-            if ( mplp.rflag_skip_any_unset <0 ) { fprintf(bcftools_stderr,"Could not parse --nf %s\n", optarg); return 1; }
+            if ( mplp.rflag_skip_any_unset <0 ) {
+                fprintf(bcftools_stderr,"Could not parse --nf %s\n", optarg);
+                goto err;
+            }
             break;
         case  17 :
             mplp.rflag_skip_all_unset = bam_str2flag(optarg);
-            if ( mplp.rflag_skip_all_unset<0 ) { fprintf(bcftools_stderr,"Could not parse --if %s\n", optarg); return 1; }
+            if ( mplp.rflag_skip_all_unset<0 ) {
+                fprintf(bcftools_stderr,"Could not parse --if %s\n", optarg);
+                goto err;
+            }
             break;
         case  18 :
             mplp.rflag_skip_any_set = bam_str2flag(optarg);
-            if ( mplp.rflag_skip_any_set <0 ) { fprintf(bcftools_stderr,"Could not parse --ef %s\n", optarg); return 1; }
+            if ( mplp.rflag_skip_any_set <0 ) {
+                fprintf(bcftools_stderr,"Could not parse --ef %s\n", optarg);
+                goto err;
+            }
             break;
         case  19 :
             mplp.rflag_skip_all_set = bam_str2flag(optarg);
-            if ( mplp.rflag_skip_all_set <0 ) { fprintf(bcftools_stderr,"Could not parse --df %s\n", optarg); return 1; }
+            if ( mplp.rflag_skip_all_set <0 ) {
+                fprintf(bcftools_stderr,"Could not parse --df %s\n", optarg);
+                goto err;
+            }
             break;
         case  3 : mplp.output_fname = optarg; break;
         case  4 : mplp.openQ = atoi(optarg); break;
@@ -1427,7 +1508,8 @@ int main_mpileup(int argc, char *argv[])
             break;
         case 'f':
             mplp.fai = fai_load(optarg);
-            if (mplp.fai == NULL) return 1;
+            if (mplp.fai == NULL)
+                goto err;
             mplp.fai_fname = optarg;
             break;
         case  7 : noref = 1; break;
@@ -1454,7 +1536,10 @@ int main_mpileup(int argc, char *argv[])
                   if ( optarg[0]=='^' ) optarg++;
                   else mplp.bed_logic = 1;
                   mplp.bed = regidx_init(optarg,NULL,NULL,0,NULL);
-                  if (!mplp.bed) { fprintf(bcftools_stderr, "bcftools mpileup: Could not read file \"%s\"", optarg); return 1; }
+                  if (!mplp.bed) {
+                      fprintf(bcftools_stderr, "bcftools mpileup: Could not read file \"%s\"", optarg);
+                      goto err;
+                  }
                   break;
         case 'P': mplp.pl_list = strdup(optarg); break;
         case 'p': mplp.flag |= MPLP_PER_SAMPLE; break;
@@ -1507,19 +1592,39 @@ int main_mpileup(int argc, char *argv[])
             else
                 mplp.indel_bias = 1/atof(optarg);
             break;
+        case 27:
+            mplp.vs_ref = atof(optarg);
+            //if (mplp.vs_ref < 0) mplp.vs_ref = 0;
+            if (mplp.vs_ref > 1) mplp.vs_ref = 1;
+            break;
         case  15: {
                 char *tmp;
                 mplp.indel_win_size = strtol(optarg,&tmp,10);
                 if ( *tmp ) error("Could not parse argument: --indel-size %s\n", optarg);
-                if ( mplp.indel_win_size < 110 )
+                if ( mplp.indel_win_size < 20 )
                 {
-                    mplp.indel_win_size = 110;
+                    mplp.indel_win_size = 20;
                     fprintf(bcftools_stderr,"Warning: running with --indel-size %d, the requested value is too small\n",mplp.indel_win_size);
                 }
             }
             break;
-        case  20: mplp.indels_v20 = 1; break;
-        case  21: mplp.write_index = 1; break;
+        case  20: mplp.indels_v20 = 1; mplp.edlib = 0; break;
+        case 'W':
+            if (!(mplp.write_index = write_index_parse(optarg)))
+                error("Unsupported index format '%s'\n", optarg);
+            break;
+        case  22: mplp.edlib = 1; mplp.indels_v20 = 0; break;
+        case  25: mplp.edlib = 0; break;
+        case  28:
+            mplp.seqQ_offset = atoi(optarg);
+            if (mplp.seqQ_offset < 100)
+                mplp.seqQ_offset = 100;
+            if (mplp.seqQ_offset > 200)
+                mplp.seqQ_offset = 200;
+            break;
+        case  23: mplp.del_bias = atof(optarg); break;
+        case  24: mplp.poly_mqual = 1; break;
+        case  26: mplp.poly_mqual = 0; break;
         case 'A': use_orphan = 1; break;
         case 'F': mplp.min_frac = atof(optarg); break;
         case 'm': mplp.min_support = atoi(optarg); break;
@@ -1528,13 +1633,13 @@ int main_mpileup(int argc, char *argv[])
         case 'a':
             if (optarg[0]=='?') {
                 list_annotations(bcftools_stderr);
-                return 1;
+                goto err;
             }
             parse_format_flag(&mplp.fmt_flag,optarg);
         break;
         case 'M': mplp.max_read_len = atoi(optarg); break;
         case 'X':
-            if (strcasecmp(optarg, "pacbio-ccs") == 0) {
+            if (strcasecmp(optarg, "pacbio-ccs-1.18") == 0) {
                 mplp.min_frac = 0.1;
                 mplp.min_baseQ = 5;
                 mplp.max_baseQ = 50;
@@ -1543,13 +1648,70 @@ int main_mpileup(int argc, char *argv[])
                 mplp.extQ = 1;
                 mplp.flag |= MPLP_REALN_PARTIAL;
                 mplp.max_read_len = 99999;
+
+            } else if (strcasecmp(optarg, "pacbio-ccs") == 0 ||
+                strcasecmp(optarg, "pacbio-ccs-1.20") == 0) {
+                mplp.min_frac = 0.1;
+                mplp.min_baseQ = 5;
+                mplp.max_baseQ = 50;
+                mplp.delta_baseQ = 10;
+                mplp.tandemQ = 300;
+                mplp.openQ = 25;
+                mplp.extQ = 1;
+                mplp.flag &= ~MPLP_REALN;
+                mplp.del_bias = 0.4;
+                mplp.indel_bias = 1/.9;
+                mplp.seqQ_offset = 118;
+                mplp.poly_mqual = 1;
+                mplp.edlib = 1;
+                mplp.vs_ref = 0.7;
+                mplp.indel_win_size = 80;
+
             } else if (strcasecmp(optarg, "ont") == 0) {
-                fprintf(bcftools_stderr, "For ONT it may be beneficial to also run bcftools call with "
+                fprintf(bcftools_stderr, "With old ONT data may be beneficial to also run bcftools call with "
                         "a higher -P, eg -P0.01 or -P 0.1\n");
                 mplp.min_baseQ = 5;
                 mplp.max_baseQ = 30;
                 mplp.flag &= ~MPLP_REALN;
                 mplp.flag |= MPLP_NO_INDEL;
+
+            } else if (strcasecmp(optarg, "ont-sup") == 0 ||
+                       strcasecmp(optarg, "ont-sup-1.20") == 0) {
+                mplp.min_frac = 0.2;
+                mplp.min_baseQ = 1;
+                mplp.max_baseQ = 35;
+                mplp.delta_baseQ = 99;
+                mplp.openQ = 15;
+                mplp.extQ = 1;
+                mplp.flag &= ~MPLP_REALN;
+                mplp.max_read_len = 9999999;
+                mplp.del_bias = 0.4;
+                mplp.poly_mqual = 1;
+                mplp.edlib = 1;
+                // If we increase -h then we can increase bias denominator too
+                mplp.tandemQ = 110;
+                mplp.indel_bias = 1/0.7;
+                mplp.seqQ_offset = 130;
+                mplp.indel_win_size = 80;
+
+            } else if (strcasecmp(optarg, "ultima") == 0 ||
+                       strcasecmp(optarg, "ultima-1.20") == 0) {
+                mplp.min_frac = 0.15;
+                mplp.min_baseQ = 1;
+                mplp.max_baseQ = 30;
+                mplp.delta_baseQ = 10;
+                mplp.openQ = 20;
+                mplp.extQ = 10;
+                mplp.tandemQ = 250;
+                mplp.flag &= ~MPLP_REALN;
+                mplp.del_bias = 0.3;
+                mplp.poly_mqual = 1;
+                mplp.edlib = 1;
+                mplp.indel_bias = 1/0.7;
+                mplp.seqQ_offset = 140;
+                mplp.vs_ref = 0.3;
+                mplp.indel_win_size = 80;
+
             } else if (strcasecmp(optarg, "1.12") == 0) {
                 // 1.12 and earlier
                 mplp.min_frac = 0.002;
@@ -1558,13 +1720,38 @@ int main_mpileup(int argc, char *argv[])
                 mplp.tandemQ = 100;
                 mplp.flag &= ~MPLP_REALN_PARTIAL;
                 mplp.flag |= MPLP_REALN;
-            } else if (strcasecmp(optarg, "illumina") == 0) {
+
+            } else if (strcasecmp(optarg, "illumina-1.18") == 0) {
+                mplp.indel_win_size = 110;
+                mplp.flag |= MPLP_REALN_PARTIAL;
+
+            } else if (strcasecmp(optarg, "illumina") == 0 ||
+                       strcasecmp(optarg, "illumina-1.20") == 0) {
+                mplp.edlib = 1;
+                mplp.indel_win_size = 110;
                 mplp.flag |= MPLP_REALN_PARTIAL;
+                mplp.indel_bias = 1;
+                mplp.seqQ_offset = 125;
+                //mplp.indel_win_size = 80; TEST?
+
+            } else if (strcasecmp(optarg, "bgi") == 0 ||
+                       strcasecmp(optarg, "bgi-1.20") == 0) {
+                mplp.min_frac = 0.1;
+                mplp.edlib = 1;
+                mplp.indel_bias = 1;
+                mplp.seqQ_offset = 120;
+                mplp.flag |= MPLP_REALN_PARTIAL;
+                mplp.indel_win_size = 80;
+
+            } else if (strcasecmp(optarg, "list") == 0 ||
+                       strcasecmp(optarg, "help") == 0) {
+                print_profiles();
+                goto err;
             } else {
                 fprintf(bcftools_stderr, "Unknown configuration name '%s'\n"
-                        "Please choose from 1.12, illumina, pacbio-ccs or ont\n",
+                        "Please use '-X list' to show available choices.\n",
                         optarg);
-                return 1;
+                goto err;
             }
             break;
         case 13: hts_srand48(atoi(optarg)); break;
@@ -1576,7 +1763,7 @@ int main_mpileup(int argc, char *argv[])
             break;
         default:
             fprintf(bcftools_stderr,"Invalid option: '%c'\n", c);
-            return 1;
+            goto err;
         }
     }
 
@@ -1601,22 +1788,23 @@ int main_mpileup(int argc, char *argv[])
     if ( !(mplp.flag&MPLP_REALN) && mplp.flag&MPLP_REDO_BAQ )
     {
         fprintf(bcftools_stderr,"Error: The -B option cannot be combined with -E\n");
-        return 1;
+        goto err;
     }
     if (use_orphan) mplp.flag &= ~MPLP_NO_ORPHAN;
     if (argc == 1)
     {
         print_usage(bcftools_stderr, &mplp);
-        return 1;
+        goto err;
     }
     if (!mplp.fai && !noref) {
         fprintf(bcftools_stderr,"Error: mpileup requires the --fasta-ref option by default; use --no-reference to run without a fasta reference\n");
-        return 1;
+        goto err;
     }
-    int ret,i;
+
     if (file_list)
     {
-        if ( read_file_list(file_list,&nfiles,&fn) ) return 1;
+        if ( read_file_list(file_list,&nfiles,&fn) )
+            goto err;
         mplp.files  = fn;
         mplp.nfiles = nfiles;
     }
@@ -1635,6 +1823,8 @@ int main_mpileup(int argc, char *argv[])
     if (mplp.bed) regidx_destroy(mplp.bed);
     if (mplp.bed_itr) regitr_destroy(mplp.bed_itr);
     if (mplp.reg) regidx_destroy(mplp.reg);
+
+ err:
     bam_smpl_destroy(mplp.bsmpl);
 
     return ret;
index 5c8133f28dde0d5dba23383066faea09a706e9e3..593b19b5f8674807196b41db4197b8d2e221112a 100644 (file)
@@ -275,7 +275,7 @@ int rcns_set_reads(read_cns_t *rcns, bam_pileup1_t *plp, int nplp)
                 }
                 y += len;
             }
-            else if ( op==BAM_CDEL )
+            else if ( op==BAM_CDEL || op==BAM_CREF_SKIP )   /* note: unsure about BAM_CREF_SKIP, don't have data to test */
             {
                 if ( x>rcns->beg && x+len-1<=rcns->end )
                 {
index a2612fd3168e8065915008b50da18e1419098ba6..ef2ff089ebd473cd3979f1e00cae00fea1eaf939 100644 (file)
@@ -277,7 +277,7 @@ int rcns_set_reads(read_cns_t *rcns, bam_pileup1_t *plp, int nplp)
                 }
                 y += len;
             }
-            else if ( op==BAM_CDEL )
+            else if ( op==BAM_CDEL || op==BAM_CREF_SKIP )   /* note: unsure about BAM_CREF_SKIP, don't have data to test */
             {
                 if ( x>rcns->beg && x+len-1<=rcns->end )
                 {
index c40bbd8660cc7a996797dd078d31f1a519bc54b8..09c43f89165c55150e2224c83d15d59f2d5cef0e 100644 (file)
@@ -136,7 +136,7 @@ int regidx_parse_vcf(const char*,char**,char**,uint32_t*,uint32_t*,void*,void*);
  *
  *  @param fname:  input file name or NULL if regions will be added one-by-one via regidx_insert()
  *  @param parsef: regidx_parse_bed, regidx_parse_tab or see description of regidx_parse_f. If NULL,
- *                 the format will be autodected, currently either regidx_parse_tab (the default) or
+ *                 the format will be autodetected, currently either regidx_parse_tab (the default) or
  *                 regidx_parse_bed (file must be named 'bed' or 'bed.gz') will be used. Note that
  *                 the exact autodetection algorithm will change.
  *  @param freef:  NULL or see description of regidx_parse_f
index ed852173ccfdaccbeede730d82676171c978928f..37e5d965e943f9dcf52645aebc80dd8a5264b1b5 100644 (file)
@@ -1,6 +1,6 @@
 /*  reheader.c -- reheader subcommand.
 
-    Copyright (C) 2014-2022 Genome Research Ltd.
+    Copyright (C) 2014-2022,2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -49,8 +49,9 @@ THE SOFTWARE.  */
 typedef struct _args_t
 {
     char **argv, *fname, *samples_fname, *header_fname, *output_fname;
-    char *fai_fname, *rm_tmpfile, *tmp_prefix;
+    char *fai_fname;
     htsFile *fp;
+    faidx_t *fai;
     htsFormat type;
     htsThreadPool *threads;
     int argc, n_threads;
@@ -168,33 +169,13 @@ char *init_tmp_prefix(const char *tmp_prefix)
     kputs("/bcftools.XXXXXX", &prefix);
     return prefix.s;
 }
-static void update_from_fai(args_t *args)
+static void update_from_fai(faidx_t *fai, kstring_t *hdr_txt)
 {
-    if ( !strcmp("-",args->fname) )
-        error("Cannot use the --fai option when reading from standard input.\n");
-
-    faidx_t *fai = fai_load3(args->fai_fname,args->fai_fname,NULL,FAI_FASTA);
-    if ( !fai ) error("Could not parse %s\n", args->fai_fname);
-    args->rm_tmpfile = init_tmp_prefix(args->tmp_prefix);
-    int fd = mkstemp(args->rm_tmpfile);
-    if ( fd<0 ) error("Could not open a temporary file for writing: %s\n", args->rm_tmpfile);
-
-    // get a template header: either from the original VCF or from --header
-    char *ori_hdr_fname = args->header_fname ? args->header_fname : args->fname;
-    htsFile *fp = hts_open(ori_hdr_fname,"r");
-    if ( !fp ) error("Failed to open: %s\n", ori_hdr_fname);
-    bcf_hdr_t *hdr = bcf_hdr_read(fp);
-    if ( !hdr ) error("Failed to read the header: %s\n", ori_hdr_fname);
-    hts_close(fp);  // no need to check the return status here
-
-    // put the header in a text buffer
-    kstring_t hdr_txt_ori = {0,0,0}, hdr_txt_new = {0,0,0};
-    bcf_hdr_format(hdr, 0, &hdr_txt_ori);
-    bcf_hdr_destroy(hdr);
+    kstring_t hdr_txt_new = {0,0,0};
 
     // update the existing contig lines and remove lines not present in the fai file
     void *chr_seen = khash_str2int_init();
-    char *tmp, *beg = hdr_txt_ori.s;
+    char *tmp, *beg = hdr_txt->s;
     while ( beg && *beg )
     {
         tmp = strstr(beg, "\n##contig=<");
@@ -216,13 +197,10 @@ static void update_from_fai(args_t *args)
     }
     kputs(tmp+1,&hdr_txt_new);
 
-    if ( write(fd, hdr_txt_new.s, hdr_txt_new.l)!=hdr_txt_new.l ) error("Failed to write %zu bytes to %s\n", hdr_txt_new.l,args->rm_tmpfile);
-    if ( close(fd)!=0 ) error("Failed to close %s\n", args->rm_tmpfile);
-    args->header_fname = args->rm_tmpfile;
+    // Switch the new header content for the old
+    free(hdr_txt->s);
+    memcpy(hdr_txt, &hdr_txt_new, sizeof(*hdr_txt));
 
-    free(hdr_txt_ori.s);
-    free(hdr_txt_new.s);
-    fai_destroy(fai);
     khash_str2int_destroy_free(chr_seen);
 }
 
@@ -420,6 +398,10 @@ static void reheader_vcf_gz(args_t *args)
         free(hdr.s); hdr.s = NULL; hdr.l = hdr.m = 0;
         read_header_file(args->header_fname, &hdr);
     }
+
+    if ( args->fai )
+        update_from_fai(args->fai, &hdr);
+
     if ( samples )
     {
         set_samples(samples, nsamples, &hdr);
@@ -433,7 +415,7 @@ static void reheader_vcf_gz(args_t *args)
     if ( bgzf_write(bgzf_out, hdr.s, hdr.l) < 0 ) error("Can't write BGZF header (code %d)\n", bgzf_out->errcode);
     free(hdr.s);
 
-    // Output all remainig data read with the header block
+    // Output all remaining data read with the header block
     if ( fp->block_length - skip_until > 0 )
     {
         if ( bgzf_write(bgzf_out, buffer+skip_until, fp->block_length-skip_until)<0 ) error("Error: %d\n",fp->errcode);
@@ -479,6 +461,10 @@ static void reheader_vcf(args_t *args)
         free(hdr.s); hdr.s = NULL; hdr.l = hdr.m = 0;
         read_header_file(args->header_fname, &hdr);
     }
+
+    if ( args->fai )
+        update_from_fai(args->fai, &hdr);
+
     if ( samples )
     {
         set_samples(samples, nsamples, &hdr);
@@ -586,6 +572,10 @@ static void reheader_bcf(args_t *args, int is_compressed)
         free(htxt.s); htxt.s = NULL; htxt.l = htxt.m = 0;
         read_header_file(args->header_fname, &htxt);
     }
+
+    if ( args->fai )
+        update_from_fai(args->fai, &htxt);
+
     if ( samples )
     {
         set_samples(samples, nsamples, &htxt);
@@ -675,11 +665,7 @@ static void usage(args_t *args)
     fprintf(stderr, "    -h, --header FILE          new header\n");
     fprintf(stderr, "    -o, --output FILE          write output to a file [standard output]\n");
     fprintf(stderr, "    -s, --samples FILE         new sample names\n");
-#ifdef _WIN32
-    fprintf(stderr, "    -T, --temp-prefix PATH     template for temporary file name [/bcftools.XXXXXX]\n");
-#else
-    fprintf(stderr, "    -T, --temp-prefix PATH     template for temporary file name [/tmp/bcftools.XXXXXX]\n");
-#endif
+    fprintf(stderr, "    -T, --temp-prefix PATH     ignored; was template for temporary file name\n");
     fprintf(stderr, "        --threads INT          use multithreading with <int> worker threads (BCF only) [0]\n");
     fprintf(stderr, "\n");
     fprintf(stderr, "Example:\n");
@@ -716,7 +702,7 @@ int main_reheader(int argc, char *argv[])
         switch (c)
         {
             case  1 : args->n_threads = strtol(optarg, 0, 0); break;
-            case 'T': args->tmp_prefix = optarg; break;
+            case 'T': break; // unused - was temp file prefix
             case 'f': args->fai_fname = optarg; break;
             case 'o': args->output_fname = optarg; break;
             case 's': args->samples_fname = optarg; break;
@@ -733,8 +719,11 @@ int main_reheader(int argc, char *argv[])
     }
     else args->fname = argv[optind];
 
-    if ( args->fai_fname ) update_from_fai(args);
-    if ( !args->samples_fname && !args->header_fname ) usage(args);
+    if ( args->fai_fname ) {
+        args->fai = fai_load3(args->fai_fname,args->fai_fname,NULL,FAI_FASTA);
+        if ( !args->fai ) error("Could not parse %s\n", args->fai_fname);
+    }
+    if ( !args->samples_fname && !args->header_fname && !args->fai) usage(args);
     if ( !args->fname ) usage(args);
 
     args->fp = hts_open(args->fname,"r");
@@ -755,11 +744,8 @@ int main_reheader(int argc, char *argv[])
     else
         reheader_bcf(args, args->type.compression==bgzf || args->type.compression==gzip);
 
-    if ( args->rm_tmpfile )
-    {
-        unlink(args->rm_tmpfile);
-        free(args->rm_tmpfile);
-    }
+    if (args->fai)
+        fai_destroy(args->fai);
     free(args);
     return 0;
 }
index 44dff8c9c5bdcf4831b2fb2e298c2648dca20bfc..87d460a80c261844e88cc2a8b0a126bf92b2b169 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  reheader.c -- reheader subcommand.
 
-    Copyright (C) 2014-2022 Genome Research Ltd.
+    Copyright (C) 2014-2022,2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -51,8 +51,9 @@ THE SOFTWARE.  */
 typedef struct _args_t
 {
     char **argv, *fname, *samples_fname, *header_fname, *output_fname;
-    char *fai_fname, *rm_tmpfile, *tmp_prefix;
+    char *fai_fname;
     htsFile *fp;
+    faidx_t *fai;
     htsFormat type;
     htsThreadPool *threads;
     int argc, n_threads;
@@ -170,33 +171,13 @@ char *init_tmp_prefix(const char *tmp_prefix)
     kputs("/bcftools.XXXXXX", &prefix);
     return prefix.s;
 }
-static void update_from_fai(args_t *args)
+static void update_from_fai(faidx_t *fai, kstring_t *hdr_txt)
 {
-    if ( !strcmp("-",args->fname) )
-        error("Cannot use the --fai option when reading from standard input.\n");
-
-    faidx_t *fai = fai_load3(args->fai_fname,args->fai_fname,NULL,FAI_FASTA);
-    if ( !fai ) error("Could not parse %s\n", args->fai_fname);
-    args->rm_tmpfile = init_tmp_prefix(args->tmp_prefix);
-    int fd = mkstemp(args->rm_tmpfile);
-    if ( fd<0 ) error("Could not open a temporary file for writing: %s\n", args->rm_tmpfile);
-
-    // get a template header: either from the original VCF or from --header
-    char *ori_hdr_fname = args->header_fname ? args->header_fname : args->fname;
-    htsFile *fp = hts_open(ori_hdr_fname,"r");
-    if ( !fp ) error("Failed to open: %s\n", ori_hdr_fname);
-    bcf_hdr_t *hdr = bcf_hdr_read(fp);
-    if ( !hdr ) error("Failed to read the header: %s\n", ori_hdr_fname);
-    hts_close(fp);  // no need to check the return status here
-
-    // put the header in a text buffer
-    kstring_t hdr_txt_ori = {0,0,0}, hdr_txt_new = {0,0,0};
-    bcf_hdr_format(hdr, 0, &hdr_txt_ori);
-    bcf_hdr_destroy(hdr);
+    kstring_t hdr_txt_new = {0,0,0};
 
     // update the existing contig lines and remove lines not present in the fai file
     void *chr_seen = khash_str2int_init();
-    char *tmp, *beg = hdr_txt_ori.s;
+    char *tmp, *beg = hdr_txt->s;
     while ( beg && *beg )
     {
         tmp = strstr(beg, "\n##contig=<");
@@ -218,13 +199,10 @@ static void update_from_fai(args_t *args)
     }
     kputs(tmp+1,&hdr_txt_new);
 
-    if ( write(fd, hdr_txt_new.s, hdr_txt_new.l)!=hdr_txt_new.l ) error("Failed to write %zu bytes to %s\n", hdr_txt_new.l,args->rm_tmpfile);
-    if ( close(fd)!=0 ) error("Failed to close %s\n", args->rm_tmpfile);
-    args->header_fname = args->rm_tmpfile;
+    // Switch the new header content for the old
+    free(hdr_txt->s);
+    memcpy(hdr_txt, &hdr_txt_new, sizeof(*hdr_txt));
 
-    free(hdr_txt_ori.s);
-    free(hdr_txt_new.s);
-    fai_destroy(fai);
     khash_str2int_destroy_free(chr_seen);
 }
 
@@ -422,6 +400,10 @@ static void reheader_vcf_gz(args_t *args)
         free(hdr.s); hdr.s = NULL; hdr.l = hdr.m = 0;
         read_header_file(args->header_fname, &hdr);
     }
+
+    if ( args->fai )
+        update_from_fai(args->fai, &hdr);
+
     if ( samples )
     {
         set_samples(samples, nsamples, &hdr);
@@ -435,7 +417,7 @@ static void reheader_vcf_gz(args_t *args)
     if ( bgzf_write(bgzf_out, hdr.s, hdr.l) < 0 ) error("Can't write BGZF header (code %d)\n", bgzf_out->errcode);
     free(hdr.s);
 
-    // Output all remainig data read with the header block
+    // Output all remaining data read with the header block
     if ( fp->block_length - skip_until > 0 )
     {
         if ( bgzf_write(bgzf_out, buffer+skip_until, fp->block_length-skip_until)<0 ) error("Error: %d\n",fp->errcode);
@@ -481,6 +463,10 @@ static void reheader_vcf(args_t *args)
         free(hdr.s); hdr.s = NULL; hdr.l = hdr.m = 0;
         read_header_file(args->header_fname, &hdr);
     }
+
+    if ( args->fai )
+        update_from_fai(args->fai, &hdr);
+
     if ( samples )
     {
         set_samples(samples, nsamples, &hdr);
@@ -588,6 +574,10 @@ static void reheader_bcf(args_t *args, int is_compressed)
         free(htxt.s); htxt.s = NULL; htxt.l = htxt.m = 0;
         read_header_file(args->header_fname, &htxt);
     }
+
+    if ( args->fai )
+        update_from_fai(args->fai, &htxt);
+
     if ( samples )
     {
         set_samples(samples, nsamples, &htxt);
@@ -677,11 +667,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "    -h, --header FILE          new header\n");
     fprintf(bcftools_stderr, "    -o, --output FILE          write output to a file [standard output]\n");
     fprintf(bcftools_stderr, "    -s, --samples FILE         new sample names\n");
-#ifdef _WIN32
-    fprintf(bcftools_stderr, "    -T, --temp-prefix PATH     template for temporary file name [/bcftools.XXXXXX]\n");
-#else
-    fprintf(bcftools_stderr, "    -T, --temp-prefix PATH     template for temporary file name [/tmp/bcftools.XXXXXX]\n");
-#endif
+    fprintf(bcftools_stderr, "    -T, --temp-prefix PATH     ignored; was template for temporary file name\n");
     fprintf(bcftools_stderr, "        --threads INT          use multithreading with <int> worker threads (BCF only) [0]\n");
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "Example:\n");
@@ -718,7 +704,7 @@ int main_reheader(int argc, char *argv[])
         switch (c)
         {
             case  1 : args->n_threads = strtol(optarg, 0, 0); break;
-            case 'T': args->tmp_prefix = optarg; break;
+            case 'T': break; // unused - was temp file prefix
             case 'f': args->fai_fname = optarg; break;
             case 'o': args->output_fname = optarg; break;
             case 's': args->samples_fname = optarg; break;
@@ -735,8 +721,11 @@ int main_reheader(int argc, char *argv[])
     }
     else args->fname = argv[optind];
 
-    if ( args->fai_fname ) update_from_fai(args);
-    if ( !args->samples_fname && !args->header_fname ) usage(args);
+    if ( args->fai_fname ) {
+        args->fai = fai_load3(args->fai_fname,args->fai_fname,NULL,FAI_FASTA);
+        if ( !args->fai ) error("Could not parse %s\n", args->fai_fname);
+    }
+    if ( !args->samples_fname && !args->header_fname && !args->fai) usage(args);
     if ( !args->fname ) usage(args);
 
     args->fp = hts_open(args->fname,"r");
@@ -757,11 +746,8 @@ int main_reheader(int argc, char *argv[])
     else
         reheader_bcf(args, args->type.compression==bgzf || args->type.compression==gzip);
 
-    if ( args->rm_tmpfile )
-    {
-        unlink(args->rm_tmpfile);
-        free(args->rm_tmpfile);
-    }
+    if (args->fai)
+        fai_destroy(args->fai);
     free(args);
     return 0;
 }
index 800cbfef9b385dd6d006a4c8537149915e179297..a9281d81164869f3a4898cb970c2141300d90fee 100644 (file)
@@ -1,7 +1,7 @@
 /*  str_finder.c -- Short Tandem Repeat finder.
     Originally from Crumble (https://github.com/jkbonfield/crumble)
 
-    Copyright (C) 2015-2016, 2021 Genome Research Ltd.
+    Copyright (C) 2015-2016, 2021-2022 Genome Research Ltd.
 
     Author: James Bonfield <jkb@sanger.ac.uk>
 
@@ -50,7 +50,7 @@ static void add_rep(rep_ele **list, char *cons, int clen, int pos, int rlen,
            return;
     }
 
-    // Find current and last occurence of repeated word.
+    // Find current and last occurrence of repeated word.
 
     cp2 = &cons[pos+1];
     // If unpadded, this is quicker: cp1 = &cons[pos+1-rlen];
@@ -137,6 +137,86 @@ static void add_rep(rep_ele **list, char *cons, int clen, int pos, int rlen,
  * Returns a list of rep_ele structs holding the start,end tuples of repeats;
  *         NULL on failure.
  */
+rep_ele *find_STR64(char *cons, int len, int lower_only) {
+    int i, j;
+    uint64_t w = 0;
+    rep_ele *reps = NULL;
+
+    for (i = j = 0; i < len && j < 26; i++) {
+       if (cons[i] == '*') continue;
+
+       w <<= 2;
+       w |= cons[i];
+       //printf("%3d %c w=%08x\n", i, cons[i], w);
+       if (j>= 1 && (w&0x0003) == ((w>> 2)&0x0003))
+           add_rep(&reps, cons, len, i, 1, lower_only, w);
+       if (j>= 3 && (w&0x000f) == ((w>> 4)&0x000f))
+           add_rep(&reps, cons, len, i, 2, lower_only, w);
+       if (j>= 5 && (w&0x003f) == ((w>> 6)&0x003f))
+           add_rep(&reps, cons, len, i, 3, lower_only, w);
+       if (j>= 7 && (w&0x00ff) == ((w>> 8)&0x00ff))
+           add_rep(&reps, cons, len, i, 4, lower_only, w);
+       if (j>= 9 && (w&0x03ff) == ((w>>10)&0x03ff))
+           add_rep(&reps, cons, len, i, 5, lower_only, w);
+       if (j>=11 && (w&0x0fff) == ((w>>12)&0x0fff))
+           add_rep(&reps, cons, len, i, 6, lower_only, w);
+       if (j>=13 && (w&0x3fff) == ((w>>14)&0x3fff))
+           add_rep(&reps, cons, len, i, 7, lower_only, w);
+       if (j>=15 && (w&0xffff) == ((w>>16)&0xffff))
+           add_rep(&reps, cons, len, i, 8, lower_only, w);
+       if (j>=17 && (w&0x003ffff) == ((w>>18)&0x003ffff))
+           add_rep(&reps, cons, len, i, 9, lower_only, w);
+       if (j>=19 && (w&0x00fffff) == ((w>>20)&0x00fffff))
+           add_rep(&reps, cons, len, i,10, lower_only, w);
+       if (j>=21 && (w&0x03fffff) == ((w>>22)&0x03fffff))
+           add_rep(&reps, cons, len, i,11, lower_only, w);
+       if (j>=23 && (w&0x0ffffff) == ((w>>24)&0x0ffffff))
+           add_rep(&reps, cons, len, i,12, lower_only, w);
+       if (j>=24 && (w&0x3ffffff) == ((w>>26)&0x3ffffff))
+           add_rep(&reps, cons, len, i,13, lower_only, w);
+
+       j++;
+    }
+
+    for (; i < len; i++) {     
+       if (cons[i] == '*') continue;
+
+       w <<= 2;
+       w |= cons[i];
+       //printf("%3d %c w=%08x\n", i, cons[i], w);
+       if      ((w&0xfffffff) == ((w>>28)&0xfffffff))
+           add_rep(&reps, cons, len, i, 14, lower_only, w);
+       else if ((w&0x3ffffff) == ((w>>26)&0x3ffffff))
+           add_rep(&reps, cons, len, i, 13, lower_only, w);
+       else if ((w&0x0ffffff) == ((w>>24)&0x0ffffff))
+           add_rep(&reps, cons, len, i, 12, lower_only, w);
+       else if ((w&0x03fffff) == ((w>>22)&0x03fffff))
+           add_rep(&reps, cons, len, i, 11, lower_only, w);
+       else if ((w&0x00fffff) == ((w>>20)&0x00fffff))
+           add_rep(&reps, cons, len, i, 10, lower_only, w);
+       else if ((w&0x003ffff) == ((w>>18)&0x003ffff))
+           add_rep(&reps, cons, len, i, 9, lower_only, w);
+       else if ((w&0xffff) == ((w>>16)&0xffff))
+           add_rep(&reps, cons, len, i, 8, lower_only, w);
+       else if ((w&0x3fff) == ((w>>14)&0x3fff))
+           add_rep(&reps, cons, len, i, 7, lower_only, w);
+       else if ((w&0x0fff) == ((w>>12)&0x0fff))
+           add_rep(&reps, cons, len, i, 6, lower_only, w);
+       else if ((w&0x03ff) == ((w>>10)&0x03ff))
+           add_rep(&reps, cons, len, i, 5, lower_only, w);
+       else if ((w&0x00ff) == ((w>> 8)&0x00ff))
+           add_rep(&reps, cons, len, i, 4, lower_only, w);
+       else if ((w&0x003f) == ((w>> 6)&0x003f))
+           add_rep(&reps, cons, len, i, 3, lower_only, w);
+       else if ((w&0x000f) == ((w>> 4)&0x000f))
+           add_rep(&reps, cons, len, i, 2, lower_only, w);
+       else if ((w&0x0003) == ((w>> 2)&0x0003))
+           add_rep(&reps, cons, len, i, 1, lower_only, w);
+    }
+
+    return reps;
+}
+
 rep_ele *find_STR(char *cons, int len, int lower_only) {
     int i, j;
     uint32_t w = 0;
@@ -172,21 +252,21 @@ rep_ele *find_STR(char *cons, int len, int lower_only) {
        w <<= 2;
        w |= cons[i];
        //printf("%3d %c w=%08x\n", i, cons[i], w);
-       if ((w&0xffff) == ((w>>16)&0xffff)) 
+       if ((w&0xffff) == ((w>>16)&0xffff))
            add_rep(&reps, cons, len, i, 8, lower_only, w);
-       else if ((w&0x3fff) == ((w>>14)&0x3fff)) 
+       else if ((w&0x3fff) == ((w>>14)&0x3fff))
            add_rep(&reps, cons, len, i, 7, lower_only, w);
-       else if ((w&0x0fff) == ((w>>12)&0x0fff)) 
+       else if ((w&0x0fff) == ((w>>12)&0x0fff))
            add_rep(&reps, cons, len, i, 6, lower_only, w);
-       else if ((w&0x03ff) == ((w>>10)&0x03ff)) 
+       else if ((w&0x03ff) == ((w>>10)&0x03ff))
            add_rep(&reps, cons, len, i, 5, lower_only, w);
-       else if ((w&0x00ff) == ((w>> 8)&0x00ff)) 
+       else if ((w&0x00ff) == ((w>> 8)&0x00ff))
            add_rep(&reps, cons, len, i, 4, lower_only, w);
-       else if ((w&0x003f) == ((w>> 6)&0x003f)) 
+       else if ((w&0x003f) == ((w>> 6)&0x003f))
            add_rep(&reps, cons, len, i, 3, lower_only, w);
-       else if ((w&0x000f) == ((w>> 4)&0x000f)) 
+       else if ((w&0x000f) == ((w>> 4)&0x000f))
            add_rep(&reps, cons, len, i, 2, lower_only, w);
-       else if ((w&0x0003) == ((w>> 2)&0x0003)) 
+       else if ((w&0x0003) == ((w>> 2)&0x0003))
            add_rep(&reps, cons, len, i, 1, lower_only, w);
     }
 
index 296c8673ae0f8bc2c07a6bf38b312663f2bd64b6..9214d0a018f2b3f87476010d0ef82c00301c0ea5 100644 (file)
@@ -3,7 +3,7 @@
 /*  str_finder.c -- Short Tandem Repeat finder.
     Originally from Crumble (https://github.com/jkbonfield/crumble)
 
-    Copyright (C) 2015-2016, 2021 Genome Research Ltd.
+    Copyright (C) 2015-2016, 2021-2022 Genome Research Ltd.
 
     Author: James Bonfield <jkb@sanger.ac.uk>
 
@@ -52,7 +52,7 @@ static void add_rep(rep_ele **list, char *cons, int clen, int pos, int rlen,
            return;
     }
 
-    // Find current and last occurence of repeated word.
+    // Find current and last occurrence of repeated word.
 
     cp2 = &cons[pos+1];
     // If unpadded, this is quicker: cp1 = &cons[pos+1-rlen];
@@ -139,6 +139,86 @@ static void add_rep(rep_ele **list, char *cons, int clen, int pos, int rlen,
  * Returns a list of rep_ele structs holding the start,end tuples of repeats;
  *         NULL on failure.
  */
+rep_ele *find_STR64(char *cons, int len, int lower_only) {
+    int i, j;
+    uint64_t w = 0;
+    rep_ele *reps = NULL;
+
+    for (i = j = 0; i < len && j < 26; i++) {
+       if (cons[i] == '*') continue;
+
+       w <<= 2;
+       w |= cons[i];
+       //printf("%3d %c w=%08x\n", i, cons[i], w);
+       if (j>= 1 && (w&0x0003) == ((w>> 2)&0x0003))
+           add_rep(&reps, cons, len, i, 1, lower_only, w);
+       if (j>= 3 && (w&0x000f) == ((w>> 4)&0x000f))
+           add_rep(&reps, cons, len, i, 2, lower_only, w);
+       if (j>= 5 && (w&0x003f) == ((w>> 6)&0x003f))
+           add_rep(&reps, cons, len, i, 3, lower_only, w);
+       if (j>= 7 && (w&0x00ff) == ((w>> 8)&0x00ff))
+           add_rep(&reps, cons, len, i, 4, lower_only, w);
+       if (j>= 9 && (w&0x03ff) == ((w>>10)&0x03ff))
+           add_rep(&reps, cons, len, i, 5, lower_only, w);
+       if (j>=11 && (w&0x0fff) == ((w>>12)&0x0fff))
+           add_rep(&reps, cons, len, i, 6, lower_only, w);
+       if (j>=13 && (w&0x3fff) == ((w>>14)&0x3fff))
+           add_rep(&reps, cons, len, i, 7, lower_only, w);
+       if (j>=15 && (w&0xffff) == ((w>>16)&0xffff))
+           add_rep(&reps, cons, len, i, 8, lower_only, w);
+       if (j>=17 && (w&0x003ffff) == ((w>>18)&0x003ffff))
+           add_rep(&reps, cons, len, i, 9, lower_only, w);
+       if (j>=19 && (w&0x00fffff) == ((w>>20)&0x00fffff))
+           add_rep(&reps, cons, len, i,10, lower_only, w);
+       if (j>=21 && (w&0x03fffff) == ((w>>22)&0x03fffff))
+           add_rep(&reps, cons, len, i,11, lower_only, w);
+       if (j>=23 && (w&0x0ffffff) == ((w>>24)&0x0ffffff))
+           add_rep(&reps, cons, len, i,12, lower_only, w);
+       if (j>=24 && (w&0x3ffffff) == ((w>>26)&0x3ffffff))
+           add_rep(&reps, cons, len, i,13, lower_only, w);
+
+       j++;
+    }
+
+    for (; i < len; i++) {     
+       if (cons[i] == '*') continue;
+
+       w <<= 2;
+       w |= cons[i];
+       //printf("%3d %c w=%08x\n", i, cons[i], w);
+       if      ((w&0xfffffff) == ((w>>28)&0xfffffff))
+           add_rep(&reps, cons, len, i, 14, lower_only, w);
+       else if ((w&0x3ffffff) == ((w>>26)&0x3ffffff))
+           add_rep(&reps, cons, len, i, 13, lower_only, w);
+       else if ((w&0x0ffffff) == ((w>>24)&0x0ffffff))
+           add_rep(&reps, cons, len, i, 12, lower_only, w);
+       else if ((w&0x03fffff) == ((w>>22)&0x03fffff))
+           add_rep(&reps, cons, len, i, 11, lower_only, w);
+       else if ((w&0x00fffff) == ((w>>20)&0x00fffff))
+           add_rep(&reps, cons, len, i, 10, lower_only, w);
+       else if ((w&0x003ffff) == ((w>>18)&0x003ffff))
+           add_rep(&reps, cons, len, i, 9, lower_only, w);
+       else if ((w&0xffff) == ((w>>16)&0xffff))
+           add_rep(&reps, cons, len, i, 8, lower_only, w);
+       else if ((w&0x3fff) == ((w>>14)&0x3fff))
+           add_rep(&reps, cons, len, i, 7, lower_only, w);
+       else if ((w&0x0fff) == ((w>>12)&0x0fff))
+           add_rep(&reps, cons, len, i, 6, lower_only, w);
+       else if ((w&0x03ff) == ((w>>10)&0x03ff))
+           add_rep(&reps, cons, len, i, 5, lower_only, w);
+       else if ((w&0x00ff) == ((w>> 8)&0x00ff))
+           add_rep(&reps, cons, len, i, 4, lower_only, w);
+       else if ((w&0x003f) == ((w>> 6)&0x003f))
+           add_rep(&reps, cons, len, i, 3, lower_only, w);
+       else if ((w&0x000f) == ((w>> 4)&0x000f))
+           add_rep(&reps, cons, len, i, 2, lower_only, w);
+       else if ((w&0x0003) == ((w>> 2)&0x0003))
+           add_rep(&reps, cons, len, i, 1, lower_only, w);
+    }
+
+    return reps;
+}
+
 rep_ele *find_STR(char *cons, int len, int lower_only) {
     int i, j;
     uint32_t w = 0;
@@ -174,21 +254,21 @@ rep_ele *find_STR(char *cons, int len, int lower_only) {
        w <<= 2;
        w |= cons[i];
        //printf("%3d %c w=%08x\n", i, cons[i], w);
-       if ((w&0xffff) == ((w>>16)&0xffff)) 
+       if ((w&0xffff) == ((w>>16)&0xffff))
            add_rep(&reps, cons, len, i, 8, lower_only, w);
-       else if ((w&0x3fff) == ((w>>14)&0x3fff)) 
+       else if ((w&0x3fff) == ((w>>14)&0x3fff))
            add_rep(&reps, cons, len, i, 7, lower_only, w);
-       else if ((w&0x0fff) == ((w>>12)&0x0fff)) 
+       else if ((w&0x0fff) == ((w>>12)&0x0fff))
            add_rep(&reps, cons, len, i, 6, lower_only, w);
-       else if ((w&0x03ff) == ((w>>10)&0x03ff)) 
+       else if ((w&0x03ff) == ((w>>10)&0x03ff))
            add_rep(&reps, cons, len, i, 5, lower_only, w);
-       else if ((w&0x00ff) == ((w>> 8)&0x00ff)) 
+       else if ((w&0x00ff) == ((w>> 8)&0x00ff))
            add_rep(&reps, cons, len, i, 4, lower_only, w);
-       else if ((w&0x003f) == ((w>> 6)&0x003f)) 
+       else if ((w&0x003f) == ((w>> 6)&0x003f))
            add_rep(&reps, cons, len, i, 3, lower_only, w);
-       else if ((w&0x000f) == ((w>> 4)&0x000f)) 
+       else if ((w&0x000f) == ((w>> 4)&0x000f))
            add_rep(&reps, cons, len, i, 2, lower_only, w);
-       else if ((w&0x0003) == ((w>> 2)&0x0003)) 
+       else if ((w&0x0003) == ((w>> 2)&0x0003))
            add_rep(&reps, cons, len, i, 1, lower_only, w);
     }
 
index 242f59ec1701f987e43a4e52de1c1370215e66a2..22f9f59416d047034529036560a386e4ecad8455 100644 (file)
@@ -1,7 +1,7 @@
 /*  str_finder.c -- Short Tandem Repeat finder.
     Originally from Crumble (https://github.com/jkbonfield/crumble)
 
-    Copyright (C) 2015-2016, 2021 Genome Research Ltd.
+    Copyright (C) 2015-2016, 2021, 2023 Genome Research Ltd.
 
     Author: James Bonfield <jkb@sanger.ac.uk>
 
@@ -48,6 +48,9 @@ typedef struct rep_ele {
  */
 rep_ele *find_STR(char *cons, int len, int lower_only);
 
+/* As above, but use a longer hash with longer STR elements found */
+rep_ele *find_STR64(char *cons, int len, int lower_only);
+
 /*
  * Returns an array of STR vs no-STR values.
  *         0  => non repetitive.
index a74935fb756491817e5a045ec6a0d9d7178d3aea..3c7959674c51f3248c95eb8c90c0152c902b0eca 100644 (file)
@@ -35,7 +35,7 @@
  * @file variantkey.h
  * @brief VariantKey main functions.
  *
- * The functions provided here allows to generate and process a 64 bit Unsigned Integer Keys for Human Genetic Variants.
+ * The functions provided here allow the generation and processing of a 64 bit Unsigned Integer Keys for Human Genetic Variants.
  * The VariantKey is sortable for chromosome and position,
  * and it is also fully reversible for variants with up to 11 bases between Reference and Alternate alleles.
  * It can be used to sort, search and match variant-based data easily and very quickly.
@@ -92,7 +92,7 @@ static inline uint8_t encode_numeric_chrom(const char *chrom, size_t size)
     {
         if ((chrom[i] > '9') || (chrom[i] < '0'))
         {
-            return 0; // NA: a character that is not a numebr was found.
+            return 0; // NA: a character that is not a number was found.
         }
         v = ((v * 10) + (chrom[i] - '0'));
     }
index b2e39ef7b6de2feb6434a54b80ddcbaac1c8da39..b66c8cf51fdbcd8d6f09c10964f2380e36a192b3 100644 (file)
@@ -1,6 +1,6 @@
 /*  vcfannotate.c -- Annotate and edit VCF/BCF files.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -104,6 +104,19 @@ typedef struct _annot_col_t
 }
 annot_col_t;
 
+typedef struct
+{
+    char *name;     // column name
+    int ht_type;    // type, one of BCF_HT_STR,BCF_HT_INT,BCF_HT_REAL
+    int icol;       // index of the annotation column to use
+    union {         // memory area with the current annotation value to pass to filter_test_ext
+        int i;
+        float f;
+        char *s;
+    };
+}
+ext_t;
+
 // Logic of the filters: include or exclude sites which match the filters?
 #define FLT_INCLUDE 1
 #define FLT_EXCLUDE 2
@@ -125,7 +138,7 @@ typedef struct _args_t
     regitr_t *tgt_itr;
     int tgt_is_bed;
 
-    filter_t *filter;
+    filter_t *filter, *filter_ext;  // only one is initialized, the latter contains external values to set dynamically on the fly
     char *filter_str;
     int filter_logic;   // include or exclude sites which match the filters? One of FLT_INCLUDE/FLT_EXCLUDE
     int keep_sites;
@@ -149,6 +162,11 @@ typedef struct _args_t
     convert_t *set_ids;
     int set_ids_replace;
 
+    // external values for dynamic -i/-e expressions
+    int n_ext;
+    ext_t *ext;
+    void **ext_ptr;
+
     int nsmpl_annot;
     int *sample_map, nsample_map, sample_is_file;   // map[idst] -> isrc
     uint8_t *src_smpl_pld, *dst_smpl_pld;   // for Number=G format fields
@@ -170,6 +188,7 @@ typedef struct _args_t
     int argc, drop_header, record_cmd_line, tgts_is_vcf, mark_sites_logic, force, single_overlaps;
     int columns_is_file, has_append_mode, pair_logic;
     dbuf_t *header_lines;
+    bcf1_t *current_rec;    // current record for local setters
 }
 args_t;
 
@@ -510,17 +529,21 @@ static int vcf_getter_info_str2str(args_t *args, bcf1_t *rec, annot_col_t *col,
 static int vcf_getter_id2str(args_t *args, bcf1_t *rec, annot_col_t *col, void **ptr, int *mptr)
 {
     char *str = *((char**)ptr);
-    int len = strlen(rec->d.id);
+    int i, len = strlen(rec->d.id);
     if ( len >= *mptr ) str = realloc(str, len+1);
-    strcpy(str, rec->d.id);
+    for (i=0; i<len; i++)
+        str[i] = rec->d.id[i]==';' ? ',' : rec->d.id[i];
+    str[len] = 0;
     *((char**)ptr) = str;
     *mptr = len+1;
     return len;
 }
-static int vcf_getter_filter2str(args_t *args, bcf1_t *rec, annot_col_t *col, void **ptr, int *mptr)
+inline static int vcf_getter_filter2str_core(bcf_hdr_t *hdr, bcf1_t *rec, char **ptr, int *mptr)
 {
+    if ( !(rec->unpacked & BCF_UN_FLT) ) bcf_unpack(rec, BCF_UN_FLT);
+
     kstring_t str;
-    str.s = *((char**)ptr);
+    str.s = *ptr;
     str.m = *mptr;
     str.l = 0;
 
@@ -529,16 +552,24 @@ static int vcf_getter_filter2str(args_t *args, bcf1_t *rec, annot_col_t *col, vo
     {
         for (i=0; i<rec->d.n_flt; i++)
         {
-            if (i) kputc(';', &str);
-            kputs(bcf_hdr_int2id(args->tgts_hdr,BCF_DT_ID,rec->d.flt[i]), &str);
+            if (i) kputc(',', &str);
+            kputs(bcf_hdr_int2id(hdr,BCF_DT_ID,rec->d.flt[i]), &str);
         }
     }
     else kputc('.', &str);
 
-    *((char**)ptr) = str.s;
+    *ptr  = str.s;
     *mptr = str.m;
     return str.l;
 }
+static int vcf_getter_filter2str_local(args_t *args, bcf1_t *rec, annot_col_t *col, void **ptr, int *mptr)
+{
+    return vcf_getter_filter2str_core(args->hdr_out, args->current_rec, (char**)ptr, mptr);
+}
+static int vcf_getter_filter2str(args_t *args, bcf1_t *rec, annot_col_t *col, void **ptr, int *mptr)
+{
+    return vcf_getter_filter2str_core(args->tgts_hdr, rec, (char**)ptr, mptr);
+}
 static int setter_filter(args_t *args, bcf1_t *line, annot_col_t *col, void *data)
 {
     if ( !data ) error("Error: the --merge-logic option cannot be used with FILTER (yet?)\n");
@@ -604,7 +635,7 @@ static int setter_pos(args_t *args, bcf1_t *line, annot_col_t *col, void *data)
     char *tmp;
     int pos = strtol(tab->cols[col->icol], &tmp, 10);
     if ( tmp==tab->cols[col->icol] )
-        error("Could not parse ~POS at %s:%"PRId64" .. [%s]\n",bcf_seqname(args->hdr,line),(int64_t)line->pos+1,tab->cols[col->icol]);
+        error("Could not parse -POS at %s:%"PRId64" .. [%s]\n",bcf_seqname(args->hdr,line),(int64_t)line->pos+1,tab->cols[col->icol]);
     line->pos = pos - 1;
     return 0;
 }
@@ -1155,6 +1186,29 @@ void khash_str2int_clear_free(void *_hash)
         if (kh_exist(hash, k)) free((char*)kh_key(hash, k));
     kh_clear(str2int, hash);
 }
+static const char *escape_string(const char *str, char needle[], char **rmme, size_t *len)
+{
+    kstring_t tmp = {0,0,0};
+    const char *bp = str, *ep = str;
+    while ( *ep )
+    {
+        int i = 0;
+        while ( needle[i] && needle[i]!=*ep ) i++;
+        if ( !needle[i] ) { ep++; continue; }
+        kputsn(bp,ep-bp,&tmp);
+        ksprintf(&tmp,"%%%X",*ep);
+        bp = ++ep;
+    }
+    if ( !tmp.l )
+    {
+        *len = strlen(str);
+        return str;
+    }
+    kputs(bp,&tmp);
+    *len  = tmp.l;
+    *rmme = tmp.s;
+    return tmp.s;
+}
 static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *data)
 {
     if ( (col->replace & REPLACE_MISSING) && col->number!=BCF_VL_A && col->number!=BCF_VL_R )
@@ -1168,13 +1222,17 @@ static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *d
     if ( col->replace & SET_OR_APPEND ) col->merge_method=MM_UNIQUE;
 
     annot_line_t *tab = (annot_line_t*) data;
+    const char *escaped = NULL;
+    char *rmme = NULL;
 
-    int len = 0;
+    size_t len = 0;
     if ( tab )
     {
-        len = strlen(tab->cols[col->icol]);
-        if ( !len ) return 0;
-        if ( len==1 && tab->cols[col->icol][0]=='.' && col->merge_method!=MM_APPEND_MISSING && !(col->replace & CARRY_OVER_MISSING) ) return 1;
+        char *str = tab->cols[col->icol];
+        if ( !str || !*str ) return 0;
+        if ( !str[1] && str[0]=='.' && col->merge_method!=MM_APPEND_MISSING && !(col->replace & CARRY_OVER_MISSING) ) return 1;
+        char needle[] = {';','=',0};
+        escaped = escape_string(tab->cols[col->icol],needle,&rmme,&len);
     }
 
     if ( col->merge_method!=MM_FIRST )
@@ -1188,8 +1246,12 @@ static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *d
             if ( col->merge_method==MM_UNIQUE )
             {
                 if ( !col->mm_str_hash ) col->mm_str_hash = (khash_t(str2int)*)khash_str2int_init();
-                if ( khash_str2int_has_key(col->mm_str_hash, tab->cols[col->icol]) ) return 1;
-                khash_str2int_inc(col->mm_str_hash, strdup(tab->cols[col->icol]));
+                if ( khash_str2int_has_key(col->mm_str_hash, escaped) )
+                {
+                    free(rmme);
+                    return 1;
+                }
+                khash_str2int_inc(col->mm_str_hash, strdup(escaped));
             }
 
             if ( (col->replace & SET_OR_APPEND) && !col->mm_kstr.l )
@@ -1201,17 +1263,20 @@ static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *d
             }
 
             if ( col->mm_kstr.l ) kputc(',',&col->mm_kstr);
-            kputs(tab->cols[col->icol], &col->mm_kstr);
+            kputs(escaped, &col->mm_kstr);
+            free(rmme);
             return 1;
         }
-
         if ( col->mm_kstr.l )
         {
             hts_expand(char,col->mm_kstr.l+1,args->mtmps,args->tmps);
             memcpy(args->tmps,col->mm_kstr.s,col->mm_kstr.l+1);
         }
         else
+        {
+            free(rmme);
             return 0;
+        }
 
         // flush the line
         if ( col->merge_method==MM_UNIQUE )
@@ -1222,13 +1287,13 @@ static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *d
     {
         assert(tab);
         hts_expand(char,len+1,args->mtmps,args->tmps);
-        memcpy(args->tmps,tab->cols[col->icol],len+1);
-
+        memcpy(args->tmps,escaped,len+1);
         if ( col->number==BCF_VL_A || col->number==BCF_VL_R )
             return setter_ARinfo_string(args,line,col,tab->nals,tab->als);
     }
-
-    return bcf_update_info_string(args->hdr_out,line,col->hdr_key_dst,args->tmps);
+    int ret = bcf_update_info_string(args->hdr_out,line,col->hdr_key_dst,args->tmps);
+    free(rmme);
+    return ret;
 }
 static int vcf_setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *data)
 {
@@ -1662,11 +1727,18 @@ static int setter_format_str(args_t *args, bcf1_t *line, annot_col_t *col, void
     if ( col->icol+args->nsmpl_annot > tab->ncols )
         error("Incorrect number of values for %s at %s:%"PRId64"\n",col->hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
 
+    char needle[] = {':',0};
     int ismpl;
     for (ismpl=0; ismpl<args->nsmpl_annot; ismpl++)
-        args->tmpp[ismpl] = tab->cols[col->icol + ismpl];
-
-    return core_setter_format_str(args,line,col,args->tmpp);
+    {
+        size_t len;
+        char *rmme = NULL;
+        const char *str = escape_string(tab->cols[col->icol + ismpl],needle,&rmme,&len);
+        args->tmpp[ismpl] = rmme ? rmme : strdup(str);
+    }
+    int ret = core_setter_format_str(args,line,col,args->tmpp);
+    for (ismpl=0; ismpl<args->nsmpl_annot; ismpl++) free(args->tmpp[ismpl]);
+    return ret;
 }
 static int determine_ploidy(int nals, int *vals, int nvals1, uint8_t *smpl, int nsmpl)
 {
@@ -2198,7 +2270,23 @@ static void init_columns(args_t *args)
         kputsn(ss, se-ss, &str);
         if ( !str.s[0] || !strcasecmp("-",str.s) ) ;
         else if ( !strcasecmp("CHROM",str.s) ) args->chr_idx = icol;
-        else if ( !strcasecmp("POS",str.s) ) args->beg_idx = icol;
+        else if ( !strcasecmp("POS",str.s) )
+        {
+            if ( replace==REPLACE_NON_MISSING && !args->tgts_is_vcf )
+            {
+                args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
+                annot_col_t *col = &args->cols[args->ncols-1];
+                memset(col,0,sizeof(*col));
+                col->icol = icol;
+                col->replace = replace;
+                col->setter  = setter_pos;
+                col->hdr_key_src = strdup(str.s);
+                col->hdr_key_dst = strdup(str.s);
+                args->match_end = icol;
+            }
+            else
+                args->beg_idx = icol;
+        }
         else if ( !strcasecmp("FROM",str.s) || !strcasecmp("BEG",str.s) ) args->beg_idx = icol;
         else if ( !strcasecmp("TO",str.s) || !strcasecmp("END",str.s) ) args->end_idx = icol;
         else if ( !strcasecmp("REF",str.s) )
@@ -2257,9 +2345,23 @@ static void init_columns(args_t *args)
             col->hdr_key_dst = strdup(str.s);
             args->match_end = icol;
         }
-        else if ( !strcasecmp("~POS",str.s) && !args->tgts_is_vcf )
+        else if ( !strcasecmp("~POS",str.s) )
+        {
+            error("Error: the use of ~POS has been deprecated, use -POS to transfer the column POS.\n");
+        }
+        else if ( str.s[0]=='~' )
         {
-            if ( args->tgts_is_vcf ) error("Error: cannot use ~POS, position can be replaced only from a tab-delimited file\n");
+            args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
+            annot_col_t *col = &args->cols[args->ncols-1];
+            memset(col,0,sizeof(*col));
+            col->icol = icol;
+            col->replace = MATCH_VALUE;
+            col->setter  = NULL;
+            col->hdr_key_src = strdup(str.s+1);
+        }
+        else if ( !strcasecmp("-POS",str.s) && !args->tgts_is_vcf )
+        {
+            if ( args->tgts_is_vcf ) error("Error: cannot use -POS, position can be replaced only from a tab-delimited file\n");
             args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
             annot_col_t *col = &args->cols[args->ncols-1];
             memset(col,0,sizeof(*col));
@@ -2290,10 +2392,30 @@ static void init_columns(args_t *args)
             if ( bcf_hdr_id2type(args->tgts_hdr,BCF_HL_INFO,hdr_id)!=BCF_HT_STR )
                 error("Only Type=String tags can be used to annotate the ID column\n");
         }
-        else if ( (ptr=strstr(str.s,":=")) && !args->targets_fname )
+        else if ( (ptr=strstr(str.s,":=")) && (!args->targets_fname || !strncasecmp(ptr+2,"./",2)) )
         {
             *ptr = 0;
-            rename_annots_push(args,ptr+2,str.s);
+            if ( !strncasecmp(str.s,"INFO/",5) && (!strcasecmp(ptr+2,"FILTER") || !strcasecmp(ptr+2,"./FILTER")) )
+            {
+                // -a not present and transferring filter, needs to be a local transfer
+                args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
+                annot_col_t *col = &args->cols[args->ncols-1];
+                memset(col,0,sizeof(*col));
+                col->icol = icol;
+                col->replace = replace;
+                col->setter = vcf_setter_info_str;
+                col->getter = vcf_getter_filter2str_local;
+                col->hdr_key_src = strdup(ptr+2);
+                col->hdr_key_dst = strdup(str.s+5);
+                tmp.l = 0;
+                ksprintf(&tmp,"##INFO=<ID=%s,Number=1,Type=String,Description=\"Transferred FILTER column\">",col->hdr_key_dst);
+                bcf_hdr_append(args->hdr_out, tmp.s);
+                if (bcf_hdr_sync(args->hdr_out) < 0) error_errno("[%s] Failed to update header", __func__);
+                int hdr_id = bcf_hdr_id2int(args->hdr_out, BCF_DT_ID, col->hdr_key_dst);
+                col->number = bcf_hdr_id2length(args->hdr_out,BCF_HL_INFO,hdr_id);
+            }
+            else
+                rename_annots_push(args,ptr+2,str.s);
             *ptr = ':';
         }
         else if ( !strcasecmp("FILTER",str.s) )
@@ -2487,6 +2609,13 @@ static void init_columns(args_t *args)
                           "       (the annotation type is modified to \"Number=.\" and allele ordering is disregarded)\n");
                 fprintf(stderr,"Warning: the =INFO/TAG feature modifies the annotation to \"Number=.\" and disregards allele ordering\n");
             }
+
+            args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
+            annot_col_t *col = &args->cols[args->ncols-1];
+            memset(col,0,sizeof(*col));
+            col->icol = icol;
+            col->replace = replace;
+
             int explicit_src_info = 0;
             int explicit_dst_info = 0;
             char *key_dst;
@@ -2517,15 +2646,14 @@ static void init_columns(args_t *args)
                     key_src[-2] = ':';
                     error("Did you mean \"FMT/%s\" rather than \"%s\"?\n",str.s,str.s);
                 }
+                else if ( !strcasecmp("FILTER",key_src) && args->tgts_is_vcf )
+                {
+                    col->getter = vcf_getter_filter2str;
+                }
             }
             else
                 key_src = key_dst;
 
-            args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
-            annot_col_t *col = &args->cols[args->ncols-1];
-            memset(col,0,sizeof(*col));
-            col->icol = icol;
-            col->replace = replace;
             col->hdr_key_src = strdup(key_src);
             col->hdr_key_dst = strdup(key_dst);
 
@@ -2782,7 +2910,7 @@ static void rename_annots(args_t *args)
         while ( *ptr && isspace(*ptr) ) ptr++;
         if ( !*ptr ) { *rmme = ' '; error("Could not parse: %s\n", args->rename_annots_map[i]); }
         if ( rename_annots_core(args, args->rename_annots_map[i], ptr) < 0 )
-            error("Could not parse \"%s %s\", expected INFO, FORMAT, or FILTER prefix\n",args->rename_annots_map[i],ptr);
+            error("Cannot rename \"%s\" to \"%s\"\n",args->rename_annots_map[i],ptr);
     }
 }
 static void rename_annots_push(args_t *args, char *src, char *dst)
@@ -2793,6 +2921,106 @@ static void rename_annots_push(args_t *args, char *src, char *dst)
     ksprintf(&str,"%s %s",src,dst);
     args->rename_annots_map[ args->rename_annots_nmap - 1 ] = str.s;
 }
+static void init_filters(args_t *args)
+{
+    // Check if the -i/-e expressions contain external values that should be determined
+    // on the fly from the annotation file. The expressions can be given as
+    //      TAG={NAME}
+    //      TAG={str:NAME}
+    //      TAG={int:NAME}
+    //      TAG={float:NAME}
+    kstring_t str = {0,0,0};
+    char *src = strdup(args->filter_str);
+    int len = 0;
+    while (1)
+    {
+        char *beg = strchr(src+len,'{');
+        if ( !beg ) break;
+
+        // check if "{" appears inside quotes, in such case do not modify
+        char skip = 0;
+        char *tmp = src;
+        while ( tmp<beg )
+        {
+            if ( tmp[0]!='"' && tmp[0]!='\'' ) { tmp++; continue; }
+
+            // quote character found
+            int quote = tmp[0];
+            tmp++;
+            while ( *tmp && tmp[0]!=quote ) tmp++;
+            if ( !*tmp ) error("Could not parse the expression: %s\n",args->filter_str);    // unbalanced quotation; todo: check for escape char
+            len = tmp - src + 1;
+            skip = 1;
+        }
+        if ( skip ) continue;
+
+        char *end = ++beg;
+        while ( *end && *end!='}' ) end++;
+        if ( !*end ) error("Could not parse the expression: %s\n",args->filter_str);
+        *end = 0;
+
+        // explicit typing?
+        int type = -1;
+        tmp = beg;
+        while ( *tmp && *tmp!=':' ) tmp++;
+        if ( *tmp )
+        {
+            *tmp = 0;
+            if ( !strcasecmp(beg,"str") ) type = BCF_HT_STR;
+            else if ( !strcasecmp(beg,"int") ) type = BCF_HT_INT;
+            else if ( !strcasecmp(beg,"float") ) type = BCF_HT_REAL;
+        }
+        args->n_ext++;
+        args->ext = (ext_t*)realloc(args->ext,sizeof(*args->ext)*args->n_ext);
+        ext_t *ext = &args->ext[args->n_ext-1];
+        ext->ht_type = type;
+        ext->name = strdup(beg);
+        if ( beg-1 > src ) kputsn(src,beg-1-src,&str);
+        if ( type==-1 ) kputs("{}",&str);
+        else if ( type==BCF_HT_STR ) kputs("{str}",&str);
+        else if ( type==BCF_HT_INT ) kputs("{int}",&str);
+        else if ( type==BCF_HT_REAL ) kputs("{float}",&str);
+        len = str.l;
+        kputs(end+1,&str);
+        free(src);
+        src = strdup(str.s);
+        str.l = 0;
+    }
+    args->filter = filter_init(args->hdr, src);
+    free(src);
+    free(str.s);
+
+    int i,j,n_ext;
+    const int *ext_type = filter_ext_types(args->filter, &n_ext);
+    if ( n_ext != args->n_ext )
+        error("Failed to parse the expression, unexpected number of dynamic variables (%d vs %d): %s\n",n_ext,args->n_ext,args->filter_str);
+
+    if ( !args->n_ext ) return;
+
+    if ( !args->tgts )
+        error("Error: dynamic variables in -i/-e expressions can be currently used only with tab-delimited file, not with VCF (todo)\n");
+
+    // contains external values
+    args->ext_ptr = malloc(sizeof(*args->ext_ptr)*args->n_ext);
+    for (i=0; i<args->n_ext; i++) args->ext[i].ht_type = ext_type[i];
+    args->filter_ext = args->filter;
+    args->filter = NULL;
+
+    // set the column idx
+    if ( args->ncols )
+    {
+        for (i=0; i<args->n_ext; i++)
+        {
+            for (j=0; j<args->ncols; j++)
+            {
+                if ( strcmp(args->ext[i].name,args->cols[j].hdr_key_src) ) continue;
+                args->ext[i].icol = args->cols[j].icol;
+                break;
+            }
+            if ( j==args->ncols ) error("No such column: %s\n",args->ext[i].name);
+        }
+    }
+}
 
 static void init_data(args_t *args)
 {
@@ -2861,7 +3089,7 @@ static void init_data(args_t *args)
     args->vcmp = vcmp_init();
 
     if ( args->filter_str )
-        args->filter = filter_init(args->hdr, args->filter_str);
+        init_filters(args);
 
     if ( args->mark_sites )
     {
@@ -2890,13 +3118,22 @@ static void init_data(args_t *args)
         if ( args->n_threads )
             hts_set_opt(args->out_fh, HTS_OPT_THREAD_POOL, args->files->p);
         if ( bcf_hdr_write(args->out_fh, args->hdr_out)!=0 ) error("[%s] Error: failed to write the header to %s\n", __func__,args->output_fname);
-        if ( args->write_index && init_index(args->out_fh,args->hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+        if ( init_index2(args->out_fh,args->hdr,args->output_fname,
+                         &args->index_fn, args->write_index) < 0 )
+            error("Error: failed to initialise index for %s\n",args->output_fname);
     }
 }
 
 static void destroy_data(args_t *args)
 {
     int i;
+    for (i=0; i<args->n_ext; i++)
+    {
+        free(args->ext[i].name);
+        if ( args->ext[i].ht_type!=BCF_HT_STR ) continue;
+    }
+    free(args->ext_ptr);
+    free(args->ext);
     for (i=0; i<args->nrm; i++) free(args->rm[i].key);
     free(args->rm);
     if ( args->hdr_out ) bcf_hdr_destroy(args->hdr_out);
@@ -2951,8 +3188,8 @@ static void destroy_data(args_t *args)
     free(args->dst_smpl_pld);
     if ( args->set_ids )
         convert_destroy(args->set_ids);
-    if ( args->filter )
-        filter_destroy(args->filter);
+    if ( args->filter ) filter_destroy(args->filter);
+    if ( args->filter_ext ) filter_destroy(args->filter_ext);
     if (args->out_fh)
     {
         if ( args->write_index )
@@ -3031,7 +3268,7 @@ static void buffer_annot_lines(args_t *args, bcf1_t *line, int start_pos, int en
         }
         else i++;
     }
-    if ( args->ref_idx==-1 && args->nalines ) return;
+    if ( !args->filter_ext && args->ref_idx==-1 && args->nalines ) return;
 
     while ( !bcf_sr_regions_overlap(args->tgts, bcf_seqname(args->hdr,line), start_pos,end_pos) )
     {
@@ -3043,7 +3280,7 @@ static void buffer_annot_lines(args_t *args, bcf1_t *line, int start_pos, int en
         tmp->start = args->tgts->start;
         tmp->end   = args->tgts->end;
         parse_annot_line(args, args->tgts->line.s, tmp);
-        if ( args->ref_idx != -1 )
+        if ( args->filter_ext || args->ref_idx != -1 )
         {
             int iseq = args->tgts->iseq;
             if ( bcf_sr_regions_next(args->tgts)<0 || args->tgts->iseq!=iseq ) break;
@@ -3082,164 +3319,181 @@ static int strstr_match(char *a, char *b)
     }
     return 0;
 }
-static void annotate(args_t *args, bcf1_t *line)
+static int annotate_from_regidx(args_t *args, bcf1_t *line)
 {
-    int i, j;
-    for (i=0; i<args->nrm; i++)
-        args->rm[i].handler(args, line, &args->rm[i]);
-
+    int j;
     int has_overlap = 0;
-    if ( args->tgt_idx )
+
+    for (j=0; j<args->ncols; j++) args->cols[j].done = 0;
+    if ( regidx_overlap(args->tgt_idx, bcf_seqname(args->hdr,line),line->pos,line->pos+line->rlen-1, args->tgt_itr) )
     {
-        for (j=0; j<args->ncols; j++) args->cols[j].done = 0;
-        if ( regidx_overlap(args->tgt_idx, bcf_seqname(args->hdr,line),line->pos,line->pos+line->rlen-1, args->tgt_itr) )
+        hts_pos_t vcf_end = line->pos + line->rlen - 1;
+        while ( regitr_overlap(args->tgt_itr) )
         {
-            hts_pos_t vcf_end = line->pos + line->rlen - 1;
-            while ( regitr_overlap(args->tgt_itr) )
-            {
-                annot_line_t *tmp = &args->alines[0];
-                tmp->rid   = line->rid;
-                tmp->start = args->tgt_itr->beg;
-                tmp->end   = args->tgt_itr->end;
+            annot_line_t *tmp = &args->alines[0];
+            tmp->rid   = line->rid;
+            tmp->start = args->tgt_itr->beg;
+            tmp->end   = args->tgt_itr->end;
 
-                // Check min overlap
-                int len_ann = tmp->end - tmp->start + 1;
-                int len_vcf = line->rlen;
-                int isec = (tmp->end < vcf_end ? tmp->end : vcf_end) - (tmp->start > line->pos ? tmp->start : line->pos) + 1;
-                assert( isec > 0 );
-                if ( args->min_overlap_ann && args->min_overlap_ann > (float)isec/len_ann ) continue;
-                if ( args->min_overlap_vcf && args->min_overlap_vcf > (float)isec/len_vcf ) continue;
+            // Check min overlap
+            int len_ann = tmp->end - tmp->start + 1;
+            int len_vcf = line->rlen;
+            int isec = (tmp->end < vcf_end ? tmp->end : vcf_end) - (tmp->start > line->pos ? tmp->start : line->pos) + 1;
+            assert( isec > 0 );
+            if ( args->min_overlap_ann && args->min_overlap_ann > (float)isec/len_ann ) continue;
+            if ( args->min_overlap_vcf && args->min_overlap_vcf > (float)isec/len_vcf ) continue;
 
-                parse_annot_line(args, regitr_payload(args->tgt_itr,char*), tmp);
-                for (j=0; j<args->ncols; j++)
-                {
-                    if ( args->cols[j].done==1 ) continue;
-                    int ret = args->cols[j].setter(args,line,&args->cols[j],tmp);
-                    if ( ret < 0 )
-                        error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
-                    if ( ret==0 )
-                        args->cols[j].done = 1;
-                    has_overlap = 1;
-                }
+            parse_annot_line(args, regitr_payload(args->tgt_itr,char*), tmp);
+
+            // If a plain BED file is provided and we are asked to just mark overlapping sites, there are
+            // no additional columns. Not sure if there can be any side effects for ill-formatted BED files
+            // with variable number of columns
+            if ( !args->ncols && args->mark_sites ) has_overlap = 1;
+
+            for (j=0; j<args->ncols; j++)
+            {
+                if ( args->cols[j].done==1 ) continue;
+                int ret = args->cols[j].setter(args,line,&args->cols[j],tmp);
+                if ( ret < 0 )
+                    error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+                if ( ret==0 )
+                    args->cols[j].done = 1;
+                has_overlap = 1;
             }
         }
-        for (j=0; j<args->ncols; j++)
+    }
+    for (j=0; j<args->ncols; j++)
+    {
+        if ( args->cols[j].done==1 || args->cols[j].merge_method == MM_FIRST ) continue;
+        if ( !args->cols[j].setter ) continue;
+        if ( args->cols[j].setter(args,line,&args->cols[j],NULL) < 0 )
+            error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+    }
+    return has_overlap;
+}
+static int pass_filter_test_ext(args_t *args, bcf1_t *line, annot_line_t *ann)
+{
+    char *tmp;
+    int i;
+    for (i=0; i<args->n_ext; i++)
+    {
+        int j = args->ext[i].icol;
+        if ( args->ext[i].ht_type==BCF_HT_STR ) args->ext_ptr[i] = args->ext[i].s = ann->cols[j];
+        else if ( args->ext[i].ht_type==BCF_HT_INT )
         {
-            if ( args->cols[j].done==1 || args->cols[j].merge_method == MM_FIRST ) continue;
-            if ( !args->cols[j].setter ) continue;
-            if ( args->cols[j].setter(args,line,&args->cols[j],NULL) < 0 )
-                error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+            args->ext[i].i = strtol(ann->cols[j],&tmp,10);
+            if ( *tmp )
+            {
+                if ( strcmp(".",ann->cols[j]) ) error("Error: could not parse the annotation file, expected an integer, found \"%s\"\n",ann->cols[j]);
+                args->ext_ptr[i] = NULL;
+            }
+            else
+                args->ext_ptr[i] = &args->ext[i].i;
+        }
+        else if ( args->ext[i].ht_type==BCF_HT_REAL )
+        {
+            args->ext[i].f = strtod(ann->cols[j],&tmp);
+            if ( *tmp )
+            {
+                if ( strcmp(".",ann->cols[j]) ) error("Error: could not parse the annotation file, expected a float, found \"%s\"\n",ann->cols[j]);
+                args->ext_ptr[i] = NULL;
+            }
+            else
+                args->ext_ptr[i] = &args->ext[i].f;
         }
     }
-    else if ( args->tgts )
-    {
-        // Buffer annotation lines. When multiple ALT alleles are present in the annotation file, at least one
-        // must match some of the VCF alleles. If the append-missing mode is set (and REF+ALT is requested), the
-        // buffered lines will annotate the VCF respecting the order in ALT and when no matching line is found
-        // for an ALT, missing value is appended instead.
-        int end_pos = line->pos + line->rlen - 1;
-        buffer_annot_lines(args, line, line->pos, end_pos);
+    int pass = filter_test_ext(args->filter_ext,line,NULL,(const void**)args->ext_ptr);
+    if ( args->filter_logic==FLT_EXCLUDE ) pass = pass ? 0 : 1;
+    return pass;
+}
+static int annotate_from_tab(args_t *args, bcf1_t *line)
+{
+    int i,j;
+    int has_overlap = 0;
+
+    // Buffer annotation lines. When multiple ALT alleles are present in the annotation file, at least one
+    // must match some of the VCF alleles. If the append-missing mode is set (and REF+ALT is requested), the
+    // buffered lines will annotate the VCF respecting the order in ALT and when no matching line is found
+    // for an ALT, missing value is appended instead.
+    int end_pos = line->pos + line->rlen - 1;
+    buffer_annot_lines(args, line, line->pos, end_pos);
 
-        args->nsrt_alines = 0;
-        hts_expand(uint32_t,args->nalines,args->msrt_alines,args->srt_alines);
-        if ( args->nalines >= 0xffff || line->n_allele >= 0xffff )
-            error("Error: too many alleles or annotation lines in the buffer at %s:%"PRId64" (todo:skip?)\n",bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+    args->nsrt_alines = 0;
+    hts_expand(uint32_t,args->nalines,args->msrt_alines,args->srt_alines);
+    if ( args->nalines >= 0xffff || line->n_allele >= 0xffff )
+        error("Error: too many alleles or annotation lines in the buffer at %s:%"PRId64" (todo:skip?)\n",bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
 
-        kstring_t match_end = {0,0,0};
-        if ( args->match_end>=0 && bcf_get_info_int32(args->hdr,line,"END",&args->tmpi,&args->mtmpi)==1 )
-            kputw(args->tmpi[0],&match_end);
+    kstring_t match_end = {0,0,0};
+    if ( args->match_end>=0 && bcf_get_info_int32(args->hdr,line,"END",&args->tmpi,&args->mtmpi)==1 )
+        kputw(args->tmpi[0],&match_end);
 
-        // Find matching lines
-        for (i=0; i<args->nalines; i++)
+    // Find matching lines
+    for (i=0; i<args->nalines; i++)
+    {
+        if ( line->pos > args->alines[i].end || end_pos < args->alines[i].start ) continue;
+        if ( args->ref_idx != -1 )  // REF+ALT matching requested
         {
-            if ( line->pos > args->alines[i].end || end_pos < args->alines[i].start ) continue;
-            if ( args->ref_idx != -1 )  // REF+ALT matching requested
+            if ( line->pos!=args->alines[i].start || vcmp_set_ref(args->vcmp, line->d.allele[0], args->alines[i].als[0]) < 0 ) continue;   // refs are not compatible
+            for (j=1; j<args->alines[i].nals; j++)
             {
-                if ( line->pos!=args->alines[i].start || vcmp_set_ref(args->vcmp, line->d.allele[0], args->alines[i].als[0]) < 0 ) continue;   // refs are not compatible
-                for (j=1; j<args->alines[i].nals; j++)
+                int ialt;
+                if ( line->n_allele==1 && args->alines[i].als[j][0]=='.' && args->alines[i].als[j][1]==0 )  // match: no ALT allele in VCF and annot file has "."
+                    ialt = 0;
+                else
                 {
-                    int ialt;
-                    if ( line->n_allele==1 && args->alines[i].als[j][0]=='.' && args->alines[i].als[j][1]==0 )  // match: no ALT allele in VCF and annot file has "."
-                        ialt = 0;
-                    else
-                    {
-                        ialt = vcmp_find_allele(args->vcmp, line->d.allele+1, line->n_allele - 1, args->alines[i].als[j]);
-                        if ( ialt < 0 ) continue;
-                        ialt++;
-                    }
-                    if ( args->match_id>=0 && !strstr_match(line->d.id,args->alines[i].cols[args->match_id]) ) continue;
-                    if ( args->match_end>=0 && match_end.l && strcmp(match_end.s,args->alines[i].cols[args->match_end]) ) continue;
-                    args->srt_alines[args->nsrt_alines++] = (ialt<<16) | i;
-                    has_overlap = 1;
-                    break;
+                    ialt = vcmp_find_allele(args->vcmp, line->d.allele+1, line->n_allele - 1, args->alines[i].als[j]);
+                    if ( ialt < 0 ) continue;
+                    ialt++;
                 }
+                if ( args->match_id>=0 && !strstr_match(line->d.id,args->alines[i].cols[args->match_id]) ) continue;
+                if ( args->match_end>=0 && match_end.l && strcmp(match_end.s,args->alines[i].cols[args->match_end]) ) continue;
+                if ( args->filter_ext && !pass_filter_test_ext(args,line,&args->alines[i]) ) continue;
+                args->srt_alines[args->nsrt_alines++] = (ialt<<16) | i;
+                has_overlap = 1;
+                break;
             }
-            else    // overlap, REF+ALT matching not requested
+        }
+        else if ( args->filter_ext )
+        {
+            if ( pass_filter_test_ext(args,line,&args->alines[i]) )
             {
                 args->srt_alines[args->nsrt_alines++] = (0xffff<<16) | i;
                 has_overlap = 1;
             }
         }
+        else    // overlap, REF+ALT matching not requested
+        {
+            args->srt_alines[args->nsrt_alines++] = (0xffff<<16) | i;
+            has_overlap = 1;
+        }
+    }
 
-        free(match_end.s);
+    free(match_end.s);
+    if ( !has_overlap && args->filter_ext && !args->keep_sites ) return has_overlap;
 
-        // Sort lines if needed
+    // Sort lines if needed
+    if ( args->has_append_mode )
+    {
+        // insertion sort by VCF ALT index (top bits) and alines index (low bits)
+        uint32_t tmp;
+        for (i=1; i<args->nsrt_alines; i++)
+            for (j=i; j>0 && args->srt_alines[j] < args->srt_alines[j-1]; j--)
+                tmp = args->srt_alines[j], args->srt_alines[j] = args->srt_alines[j-1], args->srt_alines[j-1] = tmp;
+    }
+    // Annotate
+    for (j=0; j<args->ncols; j++) args->cols[j].done = 0;
+    int ialt_exp = 1;
+    for (i=0; i<args->nsrt_alines; i++)
+    {
+        int ialt = args->srt_alines[i] >> 16;
+        int ilin = args->srt_alines[i] & 0xffff;
         if ( args->has_append_mode )
         {
-            // insertion sort by VCF ALT index (top bits) and alines index (low bits)
-            uint32_t tmp;
-            for (i=1; i<args->nsrt_alines; i++)
-                for (j=i; j>0 && args->srt_alines[j] < args->srt_alines[j-1]; j--)
-                    tmp = args->srt_alines[j], args->srt_alines[j] = args->srt_alines[j-1], args->srt_alines[j-1] = tmp;
-        }
-        // Annotate
-        for (j=0; j<args->ncols; j++) args->cols[j].done = 0;
-        int ialt_exp = 1;
-        for (i=0; i<args->nsrt_alines; i++)
-        {
-            int ialt = args->srt_alines[i] >> 16;
-            int ilin = args->srt_alines[i] & 0xffff;
-            if ( args->has_append_mode )
+            if ( ialt_exp > ialt ) continue;    // multiple annotation lines for the same position
+            if ( ialt_exp < ialt )
             {
-                if ( ialt_exp > ialt ) continue;    // multiple annotation lines for the same position
-                if ( ialt_exp < ialt )
-                {
-                    // REF+ALT matching requested, append-missing mode: insert "." if no annotation line was found for the ALT
-                    while ( ialt_exp++ < ialt )
-                    {
-                        for (j=0; j<args->ncols; j++)
-                        {
-                            if ( args->cols[j].merge_method != MM_APPEND_MISSING ) continue;
-                            if ( args->cols[j].done==1 ) continue;
-                            if ( !args->cols[j].setter ) continue;
-                            int ret = args->cols[j].setter(args,line,&args->cols[j],args->aline_missing);
-                            if ( ret < 0 )
-                                error("fixme: Could not set missing %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
-                            if ( ret==0 )
-                                args->cols[j].done = 1;
-                        }
-                    }
-                }
-            }
-            for (j=0; j<args->ncols; j++)
-            {
-                if ( args->cols[j].done==1 ) continue;
-                if ( !args->cols[j].setter ) continue;
-                int ret = args->cols[j].setter(args,line,&args->cols[j],&args->alines[ilin]);
-                if ( ret < 0 )
-                    error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
-                if ( ret==0 )
-                    args->cols[j].done = 1;
-            }
-            ialt_exp = ialt + 1;
-        }
-        if ( args->nsrt_alines )
-        {
-            // In the append-missing mode fill missing values to all trailing ALTs, but only if at least one
-            // record was found. Otherwise leave the row will be left without annotation.
-            if ( args->has_append_mode && ialt_exp < line->n_allele )
-            {
-                while ( ialt_exp++ < line->n_allele )
+                // REF+ALT matching requested, append-missing mode: insert "." if no annotation line was found for the ALT
+                while ( ialt_exp++ < ialt )
                 {
                     for (j=0; j<args->ncols; j++)
                     {
@@ -3254,32 +3508,97 @@ static void annotate(args_t *args, bcf1_t *line)
                     }
                 }
             }
-            // Flush
-            for (j=0; j<args->ncols; j++)
-            {
-                if ( args->cols[j].done==1 || args->cols[j].merge_method == MM_FIRST ) continue;
-                if ( !args->cols[j].setter ) continue;
-                int ret = args->cols[j].setter(args,line,&args->cols[j],NULL);
-                if ( ret < 0 )
-                    error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
-            }
         }
+        for (j=0; j<args->ncols; j++)
+        {
+            if ( args->cols[j].done==1 ) continue;
+            if ( !args->cols[j].setter ) continue;
+            int ret = args->cols[j].setter(args,line,&args->cols[j],&args->alines[ilin]);
+            if ( ret < 0 )
+                error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+            if ( ret==0 )
+                args->cols[j].done = 1;
+        }
+        ialt_exp = ialt + 1;
     }
-    else if ( args->files->nreaders == 2 )
+    if ( args->nsrt_alines )
     {
-        if ( bcf_sr_has_line(args->files,1) )
+        // In the append-missing mode fill missing values to all trailing ALTs, but only if at least one
+        // record was found. Otherwise leave the row will be left without annotation.
+        if ( args->has_append_mode && ialt_exp < line->n_allele )
         {
-            bcf1_t *aline = bcf_sr_get_line(args->files,1);
-            for (j=0; j<args->ncols; j++)
+            while ( ialt_exp++ < line->n_allele )
             {
-                if ( !args->cols[j].setter ) continue;
-                if ( args->cols[j].setter(args,line,&args->cols[j],aline) )
-                    error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+                for (j=0; j<args->ncols; j++)
+                {
+                    if ( args->cols[j].merge_method != MM_APPEND_MISSING ) continue;
+                    if ( args->cols[j].done==1 ) continue;
+                    if ( !args->cols[j].setter ) continue;
+                    int ret = args->cols[j].setter(args,line,&args->cols[j],args->aline_missing);
+                    if ( ret < 0 )
+                        error("fixme: Could not set missing %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+                    if ( ret==0 )
+                        args->cols[j].done = 1;
+                }
             }
-
-            has_overlap = 1;
         }
+        // Flush
+        for (j=0; j<args->ncols; j++)
+        {
+            if ( args->cols[j].done==1 || args->cols[j].merge_method == MM_FIRST ) continue;
+            if ( !args->cols[j].setter ) continue;
+            int ret = args->cols[j].setter(args,line,&args->cols[j],NULL);
+            if ( ret < 0 )
+                error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+        }
+    }
+    return has_overlap;
+}
+static int annotate_from_vcf(args_t *args, bcf1_t *line)
+{
+    if ( !bcf_sr_has_line(args->files,1) ) return 0;
+    int j;
+    bcf1_t *aline = bcf_sr_get_line(args->files,1);
+    for (j=0; j<args->ncols; j++)
+    {
+        if ( !args->cols[j].setter ) continue;
+        if ( args->cols[j].setter(args,line,&args->cols[j],aline) )
+            error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
     }
+    return 1;
+}
+static int annotate_from_self(args_t *args, bcf1_t *line)
+{
+    int j;
+    for (j=0; j<args->ncols; j++)
+    {
+        if ( !args->cols[j].setter ) continue;
+        if ( args->cols[j].setter(args,line,&args->cols[j],NULL) )
+            error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+    }
+    return 0;
+}
+static int annotate_line(args_t *args, bcf1_t *line)
+{
+    args->current_rec = line;
+
+    int i;
+    for (i=0; i<args->nrm; i++)
+        args->rm[i].handler(args, line, &args->rm[i]);
+
+    int has_overlap = 0;
+    if ( args->tgt_idx )
+        has_overlap = annotate_from_regidx(args,line);
+
+    else if ( args->tgts )
+        has_overlap = annotate_from_tab(args,line);
+
+    else if ( args->files->nreaders == 2 )
+        has_overlap = annotate_from_vcf(args,line);
+
+    else if ( args->ncols )
+        has_overlap = annotate_from_self(args,line);
+
     if ( args->set_ids )
     {
         args->tmpks.l = 0;
@@ -3304,6 +3623,8 @@ static void annotate(args_t *args, bcf1_t *line)
         else
             bcf_update_info_flag(args->hdr_out,line,args->mark_sites,NULL,has_overlap?0:1);
     }
+
+    return has_overlap;
 }
 
 static void usage(args_t *args)
@@ -3340,7 +3661,7 @@ static void usage(args_t *args)
     fprintf(stderr, "       --single-overlaps           Keep memory low by avoiding complexities arising from handling multiple overlapping intervals\n");
     fprintf(stderr, "   -x, --remove LIST               List of annotations (e.g. ID,INFO/DP,FORMAT/DP,FILTER) to remove (or keep with \"^\" prefix). See man page for details\n");
     fprintf(stderr, "       --threads INT               Number of extra output compression threads [0]\n");
-    fprintf(stderr, "       --write-index               Automatically index the output files [off]\n");
+    fprintf(stderr, "   -W, --write-index[=FMT]         Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     fprintf(stderr, "Examples:\n");
     fprintf(stderr, "   http://samtools.github.io/bcftools/howtos/annotate.html\n");
@@ -3397,11 +3718,11 @@ int main_vcfannotate(int argc, char *argv[])
         {"min-overlap",required_argument,NULL,12},
         {"no-version",no_argument,NULL,8},
         {"force",no_argument,NULL,'f'},
-        {"write-index",no_argument,NULL,13},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "h:H:?o:O:r:R:a:x:c:C:i:e:S:s:I:m:kl:f",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h:H:?o:O:r:R:a:x:c:C:i:e:S:s:I:m:kl:fW::",loptions,NULL)) >= 0)
     {
         switch (c) {
             case 'f': args->force = 1; break;
@@ -3474,7 +3795,10 @@ int main_vcfannotate(int argc, char *argv[])
             case 10 : args->single_overlaps = 1; break;
             case 11 : args->rename_annots = optarg; break;
             case 12 : args->min_overlap_str = optarg; break;
-            case 13 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
         }
@@ -3543,7 +3867,8 @@ int main_vcfannotate(int argc, char *argv[])
                 continue;
             }
         }
-        annotate(args, line);
+        int keep = annotate_line(args, line);
+        if ( args->filter_ext && !args->keep_sites && !keep ) continue;
         if ( bcf_write1(args->out_fh, args->hdr_out, line)!=0 ) error("[%s] Error: failed to write to %s\n", __func__,args->output_fname);
     }
     destroy_data(args);
index 2234ddca9ef94e6bf38e13da6a4b0dd1a2b569cd..3d4d75eeb2f05a9359f432ccc9b76eff6772f003 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  vcfannotate.c -- Annotate and edit VCF/BCF files.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -106,6 +106,19 @@ typedef struct _annot_col_t
 }
 annot_col_t;
 
+typedef struct
+{
+    char *name;     // column name
+    int ht_type;    // type, one of BCF_HT_STR,BCF_HT_INT,BCF_HT_REAL
+    int icol;       // index of the annotation column to use
+    union {         // memory area with the current annotation value to pass to filter_test_ext
+        int i;
+        float f;
+        char *s;
+    };
+}
+ext_t;
+
 // Logic of the filters: include or exclude sites which match the filters?
 #define FLT_INCLUDE 1
 #define FLT_EXCLUDE 2
@@ -127,7 +140,7 @@ typedef struct _args_t
     regitr_t *tgt_itr;
     int tgt_is_bed;
 
-    filter_t *filter;
+    filter_t *filter, *filter_ext;  // only one is initialized, the latter contains external values to set dynamically on the fly
     char *filter_str;
     int filter_logic;   // include or exclude sites which match the filters? One of FLT_INCLUDE/FLT_EXCLUDE
     int keep_sites;
@@ -151,6 +164,11 @@ typedef struct _args_t
     convert_t *set_ids;
     int set_ids_replace;
 
+    // external values for dynamic -i/-e expressions
+    int n_ext;
+    ext_t *ext;
+    void **ext_ptr;
+
     int nsmpl_annot;
     int *sample_map, nsample_map, sample_is_file;   // map[idst] -> isrc
     uint8_t *src_smpl_pld, *dst_smpl_pld;   // for Number=G format fields
@@ -172,6 +190,7 @@ typedef struct _args_t
     int argc, drop_header, record_cmd_line, tgts_is_vcf, mark_sites_logic, force, single_overlaps;
     int columns_is_file, has_append_mode, pair_logic;
     dbuf_t *header_lines;
+    bcf1_t *current_rec;    // current record for local setters
 }
 args_t;
 
@@ -512,17 +531,21 @@ static int vcf_getter_info_str2str(args_t *args, bcf1_t *rec, annot_col_t *col,
 static int vcf_getter_id2str(args_t *args, bcf1_t *rec, annot_col_t *col, void **ptr, int *mptr)
 {
     char *str = *((char**)ptr);
-    int len = strlen(rec->d.id);
+    int i, len = strlen(rec->d.id);
     if ( len >= *mptr ) str = realloc(str, len+1);
-    strcpy(str, rec->d.id);
+    for (i=0; i<len; i++)
+        str[i] = rec->d.id[i]==';' ? ',' : rec->d.id[i];
+    str[len] = 0;
     *((char**)ptr) = str;
     *mptr = len+1;
     return len;
 }
-static int vcf_getter_filter2str(args_t *args, bcf1_t *rec, annot_col_t *col, void **ptr, int *mptr)
+inline static int vcf_getter_filter2str_core(bcf_hdr_t *hdr, bcf1_t *rec, char **ptr, int *mptr)
 {
+    if ( !(rec->unpacked & BCF_UN_FLT) ) bcf_unpack(rec, BCF_UN_FLT);
+
     kstring_t str;
-    str.s = *((char**)ptr);
+    str.s = *ptr;
     str.m = *mptr;
     str.l = 0;
 
@@ -531,16 +554,24 @@ static int vcf_getter_filter2str(args_t *args, bcf1_t *rec, annot_col_t *col, vo
     {
         for (i=0; i<rec->d.n_flt; i++)
         {
-            if (i) kputc(';', &str);
-            kputs(bcf_hdr_int2id(args->tgts_hdr,BCF_DT_ID,rec->d.flt[i]), &str);
+            if (i) kputc(',', &str);
+            kputs(bcf_hdr_int2id(hdr,BCF_DT_ID,rec->d.flt[i]), &str);
         }
     }
     else kputc('.', &str);
 
-    *((char**)ptr) = str.s;
+    *ptr  = str.s;
     *mptr = str.m;
     return str.l;
 }
+static int vcf_getter_filter2str_local(args_t *args, bcf1_t *rec, annot_col_t *col, void **ptr, int *mptr)
+{
+    return vcf_getter_filter2str_core(args->hdr_out, args->current_rec, (char**)ptr, mptr);
+}
+static int vcf_getter_filter2str(args_t *args, bcf1_t *rec, annot_col_t *col, void **ptr, int *mptr)
+{
+    return vcf_getter_filter2str_core(args->tgts_hdr, rec, (char**)ptr, mptr);
+}
 static int setter_filter(args_t *args, bcf1_t *line, annot_col_t *col, void *data)
 {
     if ( !data ) error("Error: the --merge-logic option cannot be used with FILTER (yet?)\n");
@@ -606,7 +637,7 @@ static int setter_pos(args_t *args, bcf1_t *line, annot_col_t *col, void *data)
     char *tmp;
     int pos = strtol(tab->cols[col->icol], &tmp, 10);
     if ( tmp==tab->cols[col->icol] )
-        error("Could not parse ~POS at %s:%"PRId64" .. [%s]\n",bcf_seqname(args->hdr,line),(int64_t)line->pos+1,tab->cols[col->icol]);
+        error("Could not parse -POS at %s:%"PRId64" .. [%s]\n",bcf_seqname(args->hdr,line),(int64_t)line->pos+1,tab->cols[col->icol]);
     line->pos = pos - 1;
     return 0;
 }
@@ -1157,6 +1188,29 @@ void khash_str2int_clear_free(void *_hash)
         if (kh_exist(hash, k)) free((char*)kh_key(hash, k));
     kh_clear(str2int, hash);
 }
+static const char *escape_string(const char *str, char needle[], char **rmme, size_t *len)
+{
+    kstring_t tmp = {0,0,0};
+    const char *bp = str, *ep = str;
+    while ( *ep )
+    {
+        int i = 0;
+        while ( needle[i] && needle[i]!=*ep ) i++;
+        if ( !needle[i] ) { ep++; continue; }
+        kputsn(bp,ep-bp,&tmp);
+        ksprintf(&tmp,"%%%X",*ep);
+        bp = ++ep;
+    }
+    if ( !tmp.l )
+    {
+        *len = strlen(str);
+        return str;
+    }
+    kputs(bp,&tmp);
+    *len  = tmp.l;
+    *rmme = tmp.s;
+    return tmp.s;
+}
 static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *data)
 {
     if ( (col->replace & REPLACE_MISSING) && col->number!=BCF_VL_A && col->number!=BCF_VL_R )
@@ -1170,13 +1224,17 @@ static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *d
     if ( col->replace & SET_OR_APPEND ) col->merge_method=MM_UNIQUE;
 
     annot_line_t *tab = (annot_line_t*) data;
+    const char *escaped = NULL;
+    char *rmme = NULL;
 
-    int len = 0;
+    size_t len = 0;
     if ( tab )
     {
-        len = strlen(tab->cols[col->icol]);
-        if ( !len ) return 0;
-        if ( len==1 && tab->cols[col->icol][0]=='.' && col->merge_method!=MM_APPEND_MISSING && !(col->replace & CARRY_OVER_MISSING) ) return 1;
+        char *str = tab->cols[col->icol];
+        if ( !str || !*str ) return 0;
+        if ( !str[1] && str[0]=='.' && col->merge_method!=MM_APPEND_MISSING && !(col->replace & CARRY_OVER_MISSING) ) return 1;
+        char needle[] = {';','=',0};
+        escaped = escape_string(tab->cols[col->icol],needle,&rmme,&len);
     }
 
     if ( col->merge_method!=MM_FIRST )
@@ -1190,8 +1248,12 @@ static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *d
             if ( col->merge_method==MM_UNIQUE )
             {
                 if ( !col->mm_str_hash ) col->mm_str_hash = (khash_t(str2int)*)khash_str2int_init();
-                if ( khash_str2int_has_key(col->mm_str_hash, tab->cols[col->icol]) ) return 1;
-                khash_str2int_inc(col->mm_str_hash, strdup(tab->cols[col->icol]));
+                if ( khash_str2int_has_key(col->mm_str_hash, escaped) )
+                {
+                    free(rmme);
+                    return 1;
+                }
+                khash_str2int_inc(col->mm_str_hash, strdup(escaped));
             }
 
             if ( (col->replace & SET_OR_APPEND) && !col->mm_kstr.l )
@@ -1203,17 +1265,20 @@ static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *d
             }
 
             if ( col->mm_kstr.l ) kputc(',',&col->mm_kstr);
-            kputs(tab->cols[col->icol], &col->mm_kstr);
+            kputs(escaped, &col->mm_kstr);
+            free(rmme);
             return 1;
         }
-
         if ( col->mm_kstr.l )
         {
             hts_expand(char,col->mm_kstr.l+1,args->mtmps,args->tmps);
             memcpy(args->tmps,col->mm_kstr.s,col->mm_kstr.l+1);
         }
         else
+        {
+            free(rmme);
             return 0;
+        }
 
         // flush the line
         if ( col->merge_method==MM_UNIQUE )
@@ -1224,13 +1289,13 @@ static int setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *d
     {
         assert(tab);
         hts_expand(char,len+1,args->mtmps,args->tmps);
-        memcpy(args->tmps,tab->cols[col->icol],len+1);
-
+        memcpy(args->tmps,escaped,len+1);
         if ( col->number==BCF_VL_A || col->number==BCF_VL_R )
             return setter_ARinfo_string(args,line,col,tab->nals,tab->als);
     }
-
-    return bcf_update_info_string(args->hdr_out,line,col->hdr_key_dst,args->tmps);
+    int ret = bcf_update_info_string(args->hdr_out,line,col->hdr_key_dst,args->tmps);
+    free(rmme);
+    return ret;
 }
 static int vcf_setter_info_str(args_t *args, bcf1_t *line, annot_col_t *col, void *data)
 {
@@ -1664,11 +1729,18 @@ static int setter_format_str(args_t *args, bcf1_t *line, annot_col_t *col, void
     if ( col->icol+args->nsmpl_annot > tab->ncols )
         error("Incorrect number of values for %s at %s:%"PRId64"\n",col->hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
 
+    char needle[] = {':',0};
     int ismpl;
     for (ismpl=0; ismpl<args->nsmpl_annot; ismpl++)
-        args->tmpp[ismpl] = tab->cols[col->icol + ismpl];
-
-    return core_setter_format_str(args,line,col,args->tmpp);
+    {
+        size_t len;
+        char *rmme = NULL;
+        const char *str = escape_string(tab->cols[col->icol + ismpl],needle,&rmme,&len);
+        args->tmpp[ismpl] = rmme ? rmme : strdup(str);
+    }
+    int ret = core_setter_format_str(args,line,col,args->tmpp);
+    for (ismpl=0; ismpl<args->nsmpl_annot; ismpl++) free(args->tmpp[ismpl]);
+    return ret;
 }
 static int determine_ploidy(int nals, int *vals, int nvals1, uint8_t *smpl, int nsmpl)
 {
@@ -2200,7 +2272,23 @@ static void init_columns(args_t *args)
         kputsn(ss, se-ss, &str);
         if ( !str.s[0] || !strcasecmp("-",str.s) ) ;
         else if ( !strcasecmp("CHROM",str.s) ) args->chr_idx = icol;
-        else if ( !strcasecmp("POS",str.s) ) args->beg_idx = icol;
+        else if ( !strcasecmp("POS",str.s) )
+        {
+            if ( replace==REPLACE_NON_MISSING && !args->tgts_is_vcf )
+            {
+                args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
+                annot_col_t *col = &args->cols[args->ncols-1];
+                memset(col,0,sizeof(*col));
+                col->icol = icol;
+                col->replace = replace;
+                col->setter  = setter_pos;
+                col->hdr_key_src = strdup(str.s);
+                col->hdr_key_dst = strdup(str.s);
+                args->match_end = icol;
+            }
+            else
+                args->beg_idx = icol;
+        }
         else if ( !strcasecmp("FROM",str.s) || !strcasecmp("BEG",str.s) ) args->beg_idx = icol;
         else if ( !strcasecmp("TO",str.s) || !strcasecmp("END",str.s) ) args->end_idx = icol;
         else if ( !strcasecmp("REF",str.s) )
@@ -2259,9 +2347,23 @@ static void init_columns(args_t *args)
             col->hdr_key_dst = strdup(str.s);
             args->match_end = icol;
         }
-        else if ( !strcasecmp("~POS",str.s) && !args->tgts_is_vcf )
+        else if ( !strcasecmp("~POS",str.s) )
+        {
+            error("Error: the use of ~POS has been deprecated, use -POS to transfer the column POS.\n");
+        }
+        else if ( str.s[0]=='~' )
         {
-            if ( args->tgts_is_vcf ) error("Error: cannot use ~POS, position can be replaced only from a tab-delimited file\n");
+            args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
+            annot_col_t *col = &args->cols[args->ncols-1];
+            memset(col,0,sizeof(*col));
+            col->icol = icol;
+            col->replace = MATCH_VALUE;
+            col->setter  = NULL;
+            col->hdr_key_src = strdup(str.s+1);
+        }
+        else if ( !strcasecmp("-POS",str.s) && !args->tgts_is_vcf )
+        {
+            if ( args->tgts_is_vcf ) error("Error: cannot use -POS, position can be replaced only from a tab-delimited file\n");
             args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
             annot_col_t *col = &args->cols[args->ncols-1];
             memset(col,0,sizeof(*col));
@@ -2292,10 +2394,30 @@ static void init_columns(args_t *args)
             if ( bcf_hdr_id2type(args->tgts_hdr,BCF_HL_INFO,hdr_id)!=BCF_HT_STR )
                 error("Only Type=String tags can be used to annotate the ID column\n");
         }
-        else if ( (ptr=strstr(str.s,":=")) && !args->targets_fname )
+        else if ( (ptr=strstr(str.s,":=")) && (!args->targets_fname || !strncasecmp(ptr+2,"./",2)) )
         {
             *ptr = 0;
-            rename_annots_push(args,ptr+2,str.s);
+            if ( !strncasecmp(str.s,"INFO/",5) && (!strcasecmp(ptr+2,"FILTER") || !strcasecmp(ptr+2,"./FILTER")) )
+            {
+                // -a not present and transferring filter, needs to be a local transfer
+                args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
+                annot_col_t *col = &args->cols[args->ncols-1];
+                memset(col,0,sizeof(*col));
+                col->icol = icol;
+                col->replace = replace;
+                col->setter = vcf_setter_info_str;
+                col->getter = vcf_getter_filter2str_local;
+                col->hdr_key_src = strdup(ptr+2);
+                col->hdr_key_dst = strdup(str.s+5);
+                tmp.l = 0;
+                ksprintf(&tmp,"##INFO=<ID=%s,Number=1,Type=String,Description=\"Transferred FILTER column\">",col->hdr_key_dst);
+                bcf_hdr_append(args->hdr_out, tmp.s);
+                if (bcf_hdr_sync(args->hdr_out) < 0) error_errno("[%s] Failed to update header", __func__);
+                int hdr_id = bcf_hdr_id2int(args->hdr_out, BCF_DT_ID, col->hdr_key_dst);
+                col->number = bcf_hdr_id2length(args->hdr_out,BCF_HL_INFO,hdr_id);
+            }
+            else
+                rename_annots_push(args,ptr+2,str.s);
             *ptr = ':';
         }
         else if ( !strcasecmp("FILTER",str.s) )
@@ -2489,6 +2611,13 @@ static void init_columns(args_t *args)
                           "       (the annotation type is modified to \"Number=.\" and allele ordering is disregarded)\n");
                 fprintf(bcftools_stderr,"Warning: the =INFO/TAG feature modifies the annotation to \"Number=.\" and disregards allele ordering\n");
             }
+
+            args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
+            annot_col_t *col = &args->cols[args->ncols-1];
+            memset(col,0,sizeof(*col));
+            col->icol = icol;
+            col->replace = replace;
+
             int explicit_src_info = 0;
             int explicit_dst_info = 0;
             char *key_dst;
@@ -2519,15 +2648,14 @@ static void init_columns(args_t *args)
                     key_src[-2] = ':';
                     error("Did you mean \"FMT/%s\" rather than \"%s\"?\n",str.s,str.s);
                 }
+                else if ( !strcasecmp("FILTER",key_src) && args->tgts_is_vcf )
+                {
+                    col->getter = vcf_getter_filter2str;
+                }
             }
             else
                 key_src = key_dst;
 
-            args->ncols++; args->cols = (annot_col_t*) realloc(args->cols,sizeof(annot_col_t)*args->ncols);
-            annot_col_t *col = &args->cols[args->ncols-1];
-            memset(col,0,sizeof(*col));
-            col->icol = icol;
-            col->replace = replace;
             col->hdr_key_src = strdup(key_src);
             col->hdr_key_dst = strdup(key_dst);
 
@@ -2784,7 +2912,7 @@ static void rename_annots(args_t *args)
         while ( *ptr && isspace(*ptr) ) ptr++;
         if ( !*ptr ) { *rmme = ' '; error("Could not parse: %s\n", args->rename_annots_map[i]); }
         if ( rename_annots_core(args, args->rename_annots_map[i], ptr) < 0 )
-            error("Could not parse \"%s %s\", expected INFO, FORMAT, or FILTER prefix\n",args->rename_annots_map[i],ptr);
+            error("Cannot rename \"%s\" to \"%s\"\n",args->rename_annots_map[i],ptr);
     }
 }
 static void rename_annots_push(args_t *args, char *src, char *dst)
@@ -2795,6 +2923,106 @@ static void rename_annots_push(args_t *args, char *src, char *dst)
     ksprintf(&str,"%s %s",src,dst);
     args->rename_annots_map[ args->rename_annots_nmap - 1 ] = str.s;
 }
+static void init_filters(args_t *args)
+{
+    // Check if the -i/-e expressions contain external values that should be determined
+    // on the fly from the annotation file. The expressions can be given as
+    //      TAG={NAME}
+    //      TAG={str:NAME}
+    //      TAG={int:NAME}
+    //      TAG={float:NAME}
+    kstring_t str = {0,0,0};
+    char *src = strdup(args->filter_str);
+    int len = 0;
+    while (1)
+    {
+        char *beg = strchr(src+len,'{');
+        if ( !beg ) break;
+
+        // check if "{" appears inside quotes, in such case do not modify
+        char skip = 0;
+        char *tmp = src;
+        while ( tmp<beg )
+        {
+            if ( tmp[0]!='"' && tmp[0]!='\'' ) { tmp++; continue; }
+
+            // quote character found
+            int quote = tmp[0];
+            tmp++;
+            while ( *tmp && tmp[0]!=quote ) tmp++;
+            if ( !*tmp ) error("Could not parse the expression: %s\n",args->filter_str);    // unbalanced quotation; todo: check for escape char
+            len = tmp - src + 1;
+            skip = 1;
+        }
+        if ( skip ) continue;
+
+        char *end = ++beg;
+        while ( *end && *end!='}' ) end++;
+        if ( !*end ) error("Could not parse the expression: %s\n",args->filter_str);
+        *end = 0;
+
+        // explicit typing?
+        int type = -1;
+        tmp = beg;
+        while ( *tmp && *tmp!=':' ) tmp++;
+        if ( *tmp )
+        {
+            *tmp = 0;
+            if ( !strcasecmp(beg,"str") ) type = BCF_HT_STR;
+            else if ( !strcasecmp(beg,"int") ) type = BCF_HT_INT;
+            else if ( !strcasecmp(beg,"float") ) type = BCF_HT_REAL;
+        }
+        args->n_ext++;
+        args->ext = (ext_t*)realloc(args->ext,sizeof(*args->ext)*args->n_ext);
+        ext_t *ext = &args->ext[args->n_ext-1];
+        ext->ht_type = type;
+        ext->name = strdup(beg);
+        if ( beg-1 > src ) kputsn(src,beg-1-src,&str);
+        if ( type==-1 ) kputs("{}",&str);
+        else if ( type==BCF_HT_STR ) kputs("{str}",&str);
+        else if ( type==BCF_HT_INT ) kputs("{int}",&str);
+        else if ( type==BCF_HT_REAL ) kputs("{float}",&str);
+        len = str.l;
+        kputs(end+1,&str);
+        free(src);
+        src = strdup(str.s);
+        str.l = 0;
+    }
+    args->filter = filter_init(args->hdr, src);
+    free(src);
+    free(str.s);
+
+    int i,j,n_ext;
+    const int *ext_type = filter_ext_types(args->filter, &n_ext);
+    if ( n_ext != args->n_ext )
+        error("Failed to parse the expression, unexpected number of dynamic variables (%d vs %d): %s\n",n_ext,args->n_ext,args->filter_str);
+
+    if ( !args->n_ext ) return;
+
+    if ( !args->tgts )
+        error("Error: dynamic variables in -i/-e expressions can be currently used only with tab-delimited file, not with VCF (todo)\n");
+
+    // contains external values
+    args->ext_ptr = malloc(sizeof(*args->ext_ptr)*args->n_ext);
+    for (i=0; i<args->n_ext; i++) args->ext[i].ht_type = ext_type[i];
+    args->filter_ext = args->filter;
+    args->filter = NULL;
+
+    // set the column idx
+    if ( args->ncols )
+    {
+        for (i=0; i<args->n_ext; i++)
+        {
+            for (j=0; j<args->ncols; j++)
+            {
+                if ( strcmp(args->ext[i].name,args->cols[j].hdr_key_src) ) continue;
+                args->ext[i].icol = args->cols[j].icol;
+                break;
+            }
+            if ( j==args->ncols ) error("No such column: %s\n",args->ext[i].name);
+        }
+    }
+}
 
 static void init_data(args_t *args)
 {
@@ -2863,7 +3091,7 @@ static void init_data(args_t *args)
     args->vcmp = vcmp_init();
 
     if ( args->filter_str )
-        args->filter = filter_init(args->hdr, args->filter_str);
+        init_filters(args);
 
     if ( args->mark_sites )
     {
@@ -2892,13 +3120,22 @@ static void init_data(args_t *args)
         if ( args->n_threads )
             hts_set_opt(args->out_fh, HTS_OPT_THREAD_POOL, args->files->p);
         if ( bcf_hdr_write(args->out_fh, args->hdr_out)!=0 ) error("[%s] Error: failed to write the header to %s\n", __func__,args->output_fname);
-        if ( args->write_index && init_index(args->out_fh,args->hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+        if ( init_index2(args->out_fh,args->hdr,args->output_fname,
+                         &args->index_fn, args->write_index) < 0 )
+            error("Error: failed to initialise index for %s\n",args->output_fname);
     }
 }
 
 static void destroy_data(args_t *args)
 {
     int i;
+    for (i=0; i<args->n_ext; i++)
+    {
+        free(args->ext[i].name);
+        if ( args->ext[i].ht_type!=BCF_HT_STR ) continue;
+    }
+    free(args->ext_ptr);
+    free(args->ext);
     for (i=0; i<args->nrm; i++) free(args->rm[i].key);
     free(args->rm);
     if ( args->hdr_out ) bcf_hdr_destroy(args->hdr_out);
@@ -2953,8 +3190,8 @@ static void destroy_data(args_t *args)
     free(args->dst_smpl_pld);
     if ( args->set_ids )
         convert_destroy(args->set_ids);
-    if ( args->filter )
-        filter_destroy(args->filter);
+    if ( args->filter ) filter_destroy(args->filter);
+    if ( args->filter_ext ) filter_destroy(args->filter_ext);
     if (args->out_fh)
     {
         if ( args->write_index )
@@ -3033,7 +3270,7 @@ static void buffer_annot_lines(args_t *args, bcf1_t *line, int start_pos, int en
         }
         else i++;
     }
-    if ( args->ref_idx==-1 && args->nalines ) return;
+    if ( !args->filter_ext && args->ref_idx==-1 && args->nalines ) return;
 
     while ( !bcf_sr_regions_overlap(args->tgts, bcf_seqname(args->hdr,line), start_pos,end_pos) )
     {
@@ -3045,7 +3282,7 @@ static void buffer_annot_lines(args_t *args, bcf1_t *line, int start_pos, int en
         tmp->start = args->tgts->start;
         tmp->end   = args->tgts->end;
         parse_annot_line(args, args->tgts->line.s, tmp);
-        if ( args->ref_idx != -1 )
+        if ( args->filter_ext || args->ref_idx != -1 )
         {
             int iseq = args->tgts->iseq;
             if ( bcf_sr_regions_next(args->tgts)<0 || args->tgts->iseq!=iseq ) break;
@@ -3084,164 +3321,181 @@ static int strstr_match(char *a, char *b)
     }
     return 0;
 }
-static void annotate(args_t *args, bcf1_t *line)
+static int annotate_from_regidx(args_t *args, bcf1_t *line)
 {
-    int i, j;
-    for (i=0; i<args->nrm; i++)
-        args->rm[i].handler(args, line, &args->rm[i]);
-
+    int j;
     int has_overlap = 0;
-    if ( args->tgt_idx )
+
+    for (j=0; j<args->ncols; j++) args->cols[j].done = 0;
+    if ( regidx_overlap(args->tgt_idx, bcf_seqname(args->hdr,line),line->pos,line->pos+line->rlen-1, args->tgt_itr) )
     {
-        for (j=0; j<args->ncols; j++) args->cols[j].done = 0;
-        if ( regidx_overlap(args->tgt_idx, bcf_seqname(args->hdr,line),line->pos,line->pos+line->rlen-1, args->tgt_itr) )
+        hts_pos_t vcf_end = line->pos + line->rlen - 1;
+        while ( regitr_overlap(args->tgt_itr) )
         {
-            hts_pos_t vcf_end = line->pos + line->rlen - 1;
-            while ( regitr_overlap(args->tgt_itr) )
-            {
-                annot_line_t *tmp = &args->alines[0];
-                tmp->rid   = line->rid;
-                tmp->start = args->tgt_itr->beg;
-                tmp->end   = args->tgt_itr->end;
+            annot_line_t *tmp = &args->alines[0];
+            tmp->rid   = line->rid;
+            tmp->start = args->tgt_itr->beg;
+            tmp->end   = args->tgt_itr->end;
 
-                // Check min overlap
-                int len_ann = tmp->end - tmp->start + 1;
-                int len_vcf = line->rlen;
-                int isec = (tmp->end < vcf_end ? tmp->end : vcf_end) - (tmp->start > line->pos ? tmp->start : line->pos) + 1;
-                assert( isec > 0 );
-                if ( args->min_overlap_ann && args->min_overlap_ann > (float)isec/len_ann ) continue;
-                if ( args->min_overlap_vcf && args->min_overlap_vcf > (float)isec/len_vcf ) continue;
+            // Check min overlap
+            int len_ann = tmp->end - tmp->start + 1;
+            int len_vcf = line->rlen;
+            int isec = (tmp->end < vcf_end ? tmp->end : vcf_end) - (tmp->start > line->pos ? tmp->start : line->pos) + 1;
+            assert( isec > 0 );
+            if ( args->min_overlap_ann && args->min_overlap_ann > (float)isec/len_ann ) continue;
+            if ( args->min_overlap_vcf && args->min_overlap_vcf > (float)isec/len_vcf ) continue;
 
-                parse_annot_line(args, regitr_payload(args->tgt_itr,char*), tmp);
-                for (j=0; j<args->ncols; j++)
-                {
-                    if ( args->cols[j].done==1 ) continue;
-                    int ret = args->cols[j].setter(args,line,&args->cols[j],tmp);
-                    if ( ret < 0 )
-                        error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
-                    if ( ret==0 )
-                        args->cols[j].done = 1;
-                    has_overlap = 1;
-                }
+            parse_annot_line(args, regitr_payload(args->tgt_itr,char*), tmp);
+
+            // If a plain BED file is provided and we are asked to just mark overlapping sites, there are
+            // no additional columns. Not sure if there can be any side effects for ill-formatted BED files
+            // with variable number of columns
+            if ( !args->ncols && args->mark_sites ) has_overlap = 1;
+
+            for (j=0; j<args->ncols; j++)
+            {
+                if ( args->cols[j].done==1 ) continue;
+                int ret = args->cols[j].setter(args,line,&args->cols[j],tmp);
+                if ( ret < 0 )
+                    error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+                if ( ret==0 )
+                    args->cols[j].done = 1;
+                has_overlap = 1;
             }
         }
-        for (j=0; j<args->ncols; j++)
+    }
+    for (j=0; j<args->ncols; j++)
+    {
+        if ( args->cols[j].done==1 || args->cols[j].merge_method == MM_FIRST ) continue;
+        if ( !args->cols[j].setter ) continue;
+        if ( args->cols[j].setter(args,line,&args->cols[j],NULL) < 0 )
+            error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+    }
+    return has_overlap;
+}
+static int pass_filter_test_ext(args_t *args, bcf1_t *line, annot_line_t *ann)
+{
+    char *tmp;
+    int i;
+    for (i=0; i<args->n_ext; i++)
+    {
+        int j = args->ext[i].icol;
+        if ( args->ext[i].ht_type==BCF_HT_STR ) args->ext_ptr[i] = args->ext[i].s = ann->cols[j];
+        else if ( args->ext[i].ht_type==BCF_HT_INT )
         {
-            if ( args->cols[j].done==1 || args->cols[j].merge_method == MM_FIRST ) continue;
-            if ( !args->cols[j].setter ) continue;
-            if ( args->cols[j].setter(args,line,&args->cols[j],NULL) < 0 )
-                error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+            args->ext[i].i = strtol(ann->cols[j],&tmp,10);
+            if ( *tmp )
+            {
+                if ( strcmp(".",ann->cols[j]) ) error("Error: could not parse the annotation file, expected an integer, found \"%s\"\n",ann->cols[j]);
+                args->ext_ptr[i] = NULL;
+            }
+            else
+                args->ext_ptr[i] = &args->ext[i].i;
+        }
+        else if ( args->ext[i].ht_type==BCF_HT_REAL )
+        {
+            args->ext[i].f = strtod(ann->cols[j],&tmp);
+            if ( *tmp )
+            {
+                if ( strcmp(".",ann->cols[j]) ) error("Error: could not parse the annotation file, expected a float, found \"%s\"\n",ann->cols[j]);
+                args->ext_ptr[i] = NULL;
+            }
+            else
+                args->ext_ptr[i] = &args->ext[i].f;
         }
     }
-    else if ( args->tgts )
-    {
-        // Buffer annotation lines. When multiple ALT alleles are present in the annotation file, at least one
-        // must match some of the VCF alleles. If the append-missing mode is set (and REF+ALT is requested), the
-        // buffered lines will annotate the VCF respecting the order in ALT and when no matching line is found
-        // for an ALT, missing value is appended instead.
-        int end_pos = line->pos + line->rlen - 1;
-        buffer_annot_lines(args, line, line->pos, end_pos);
+    int pass = filter_test_ext(args->filter_ext,line,NULL,(const void**)args->ext_ptr);
+    if ( args->filter_logic==FLT_EXCLUDE ) pass = pass ? 0 : 1;
+    return pass;
+}
+static int annotate_from_tab(args_t *args, bcf1_t *line)
+{
+    int i,j;
+    int has_overlap = 0;
+
+    // Buffer annotation lines. When multiple ALT alleles are present in the annotation file, at least one
+    // must match some of the VCF alleles. If the append-missing mode is set (and REF+ALT is requested), the
+    // buffered lines will annotate the VCF respecting the order in ALT and when no matching line is found
+    // for an ALT, missing value is appended instead.
+    int end_pos = line->pos + line->rlen - 1;
+    buffer_annot_lines(args, line, line->pos, end_pos);
 
-        args->nsrt_alines = 0;
-        hts_expand(uint32_t,args->nalines,args->msrt_alines,args->srt_alines);
-        if ( args->nalines >= 0xffff || line->n_allele >= 0xffff )
-            error("Error: too many alleles or annotation lines in the buffer at %s:%"PRId64" (todo:skip?)\n",bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+    args->nsrt_alines = 0;
+    hts_expand(uint32_t,args->nalines,args->msrt_alines,args->srt_alines);
+    if ( args->nalines >= 0xffff || line->n_allele >= 0xffff )
+        error("Error: too many alleles or annotation lines in the buffer at %s:%"PRId64" (todo:skip?)\n",bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
 
-        kstring_t match_end = {0,0,0};
-        if ( args->match_end>=0 && bcf_get_info_int32(args->hdr,line,"END",&args->tmpi,&args->mtmpi)==1 )
-            kputw(args->tmpi[0],&match_end);
+    kstring_t match_end = {0,0,0};
+    if ( args->match_end>=0 && bcf_get_info_int32(args->hdr,line,"END",&args->tmpi,&args->mtmpi)==1 )
+        kputw(args->tmpi[0],&match_end);
 
-        // Find matching lines
-        for (i=0; i<args->nalines; i++)
+    // Find matching lines
+    for (i=0; i<args->nalines; i++)
+    {
+        if ( line->pos > args->alines[i].end || end_pos < args->alines[i].start ) continue;
+        if ( args->ref_idx != -1 )  // REF+ALT matching requested
         {
-            if ( line->pos > args->alines[i].end || end_pos < args->alines[i].start ) continue;
-            if ( args->ref_idx != -1 )  // REF+ALT matching requested
+            if ( line->pos!=args->alines[i].start || vcmp_set_ref(args->vcmp, line->d.allele[0], args->alines[i].als[0]) < 0 ) continue;   // refs are not compatible
+            for (j=1; j<args->alines[i].nals; j++)
             {
-                if ( line->pos!=args->alines[i].start || vcmp_set_ref(args->vcmp, line->d.allele[0], args->alines[i].als[0]) < 0 ) continue;   // refs are not compatible
-                for (j=1; j<args->alines[i].nals; j++)
+                int ialt;
+                if ( line->n_allele==1 && args->alines[i].als[j][0]=='.' && args->alines[i].als[j][1]==0 )  // match: no ALT allele in VCF and annot file has "."
+                    ialt = 0;
+                else
                 {
-                    int ialt;
-                    if ( line->n_allele==1 && args->alines[i].als[j][0]=='.' && args->alines[i].als[j][1]==0 )  // match: no ALT allele in VCF and annot file has "."
-                        ialt = 0;
-                    else
-                    {
-                        ialt = vcmp_find_allele(args->vcmp, line->d.allele+1, line->n_allele - 1, args->alines[i].als[j]);
-                        if ( ialt < 0 ) continue;
-                        ialt++;
-                    }
-                    if ( args->match_id>=0 && !strstr_match(line->d.id,args->alines[i].cols[args->match_id]) ) continue;
-                    if ( args->match_end>=0 && match_end.l && strcmp(match_end.s,args->alines[i].cols[args->match_end]) ) continue;
-                    args->srt_alines[args->nsrt_alines++] = (ialt<<16) | i;
-                    has_overlap = 1;
-                    break;
+                    ialt = vcmp_find_allele(args->vcmp, line->d.allele+1, line->n_allele - 1, args->alines[i].als[j]);
+                    if ( ialt < 0 ) continue;
+                    ialt++;
                 }
+                if ( args->match_id>=0 && !strstr_match(line->d.id,args->alines[i].cols[args->match_id]) ) continue;
+                if ( args->match_end>=0 && match_end.l && strcmp(match_end.s,args->alines[i].cols[args->match_end]) ) continue;
+                if ( args->filter_ext && !pass_filter_test_ext(args,line,&args->alines[i]) ) continue;
+                args->srt_alines[args->nsrt_alines++] = (ialt<<16) | i;
+                has_overlap = 1;
+                break;
             }
-            else    // overlap, REF+ALT matching not requested
+        }
+        else if ( args->filter_ext )
+        {
+            if ( pass_filter_test_ext(args,line,&args->alines[i]) )
             {
                 args->srt_alines[args->nsrt_alines++] = (0xffff<<16) | i;
                 has_overlap = 1;
             }
         }
+        else    // overlap, REF+ALT matching not requested
+        {
+            args->srt_alines[args->nsrt_alines++] = (0xffff<<16) | i;
+            has_overlap = 1;
+        }
+    }
 
-        free(match_end.s);
+    free(match_end.s);
+    if ( !has_overlap && args->filter_ext && !args->keep_sites ) return has_overlap;
 
-        // Sort lines if needed
+    // Sort lines if needed
+    if ( args->has_append_mode )
+    {
+        // insertion sort by VCF ALT index (top bits) and alines index (low bits)
+        uint32_t tmp;
+        for (i=1; i<args->nsrt_alines; i++)
+            for (j=i; j>0 && args->srt_alines[j] < args->srt_alines[j-1]; j--)
+                tmp = args->srt_alines[j], args->srt_alines[j] = args->srt_alines[j-1], args->srt_alines[j-1] = tmp;
+    }
+    // Annotate
+    for (j=0; j<args->ncols; j++) args->cols[j].done = 0;
+    int ialt_exp = 1;
+    for (i=0; i<args->nsrt_alines; i++)
+    {
+        int ialt = args->srt_alines[i] >> 16;
+        int ilin = args->srt_alines[i] & 0xffff;
         if ( args->has_append_mode )
         {
-            // insertion sort by VCF ALT index (top bits) and alines index (low bits)
-            uint32_t tmp;
-            for (i=1; i<args->nsrt_alines; i++)
-                for (j=i; j>0 && args->srt_alines[j] < args->srt_alines[j-1]; j--)
-                    tmp = args->srt_alines[j], args->srt_alines[j] = args->srt_alines[j-1], args->srt_alines[j-1] = tmp;
-        }
-        // Annotate
-        for (j=0; j<args->ncols; j++) args->cols[j].done = 0;
-        int ialt_exp = 1;
-        for (i=0; i<args->nsrt_alines; i++)
-        {
-            int ialt = args->srt_alines[i] >> 16;
-            int ilin = args->srt_alines[i] & 0xffff;
-            if ( args->has_append_mode )
+            if ( ialt_exp > ialt ) continue;    // multiple annotation lines for the same position
+            if ( ialt_exp < ialt )
             {
-                if ( ialt_exp > ialt ) continue;    // multiple annotation lines for the same position
-                if ( ialt_exp < ialt )
-                {
-                    // REF+ALT matching requested, append-missing mode: insert "." if no annotation line was found for the ALT
-                    while ( ialt_exp++ < ialt )
-                    {
-                        for (j=0; j<args->ncols; j++)
-                        {
-                            if ( args->cols[j].merge_method != MM_APPEND_MISSING ) continue;
-                            if ( args->cols[j].done==1 ) continue;
-                            if ( !args->cols[j].setter ) continue;
-                            int ret = args->cols[j].setter(args,line,&args->cols[j],args->aline_missing);
-                            if ( ret < 0 )
-                                error("fixme: Could not set missing %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
-                            if ( ret==0 )
-                                args->cols[j].done = 1;
-                        }
-                    }
-                }
-            }
-            for (j=0; j<args->ncols; j++)
-            {
-                if ( args->cols[j].done==1 ) continue;
-                if ( !args->cols[j].setter ) continue;
-                int ret = args->cols[j].setter(args,line,&args->cols[j],&args->alines[ilin]);
-                if ( ret < 0 )
-                    error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
-                if ( ret==0 )
-                    args->cols[j].done = 1;
-            }
-            ialt_exp = ialt + 1;
-        }
-        if ( args->nsrt_alines )
-        {
-            // In the append-missing mode fill missing values to all trailing ALTs, but only if at least one
-            // record was found. Otherwise leave the row will be left without annotation.
-            if ( args->has_append_mode && ialt_exp < line->n_allele )
-            {
-                while ( ialt_exp++ < line->n_allele )
+                // REF+ALT matching requested, append-missing mode: insert "." if no annotation line was found for the ALT
+                while ( ialt_exp++ < ialt )
                 {
                     for (j=0; j<args->ncols; j++)
                     {
@@ -3256,32 +3510,97 @@ static void annotate(args_t *args, bcf1_t *line)
                     }
                 }
             }
-            // Flush
-            for (j=0; j<args->ncols; j++)
-            {
-                if ( args->cols[j].done==1 || args->cols[j].merge_method == MM_FIRST ) continue;
-                if ( !args->cols[j].setter ) continue;
-                int ret = args->cols[j].setter(args,line,&args->cols[j],NULL);
-                if ( ret < 0 )
-                    error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
-            }
         }
+        for (j=0; j<args->ncols; j++)
+        {
+            if ( args->cols[j].done==1 ) continue;
+            if ( !args->cols[j].setter ) continue;
+            int ret = args->cols[j].setter(args,line,&args->cols[j],&args->alines[ilin]);
+            if ( ret < 0 )
+                error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+            if ( ret==0 )
+                args->cols[j].done = 1;
+        }
+        ialt_exp = ialt + 1;
     }
-    else if ( args->files->nreaders == 2 )
+    if ( args->nsrt_alines )
     {
-        if ( bcf_sr_has_line(args->files,1) )
+        // In the append-missing mode fill missing values to all trailing ALTs, but only if at least one
+        // record was found. Otherwise leave the row will be left without annotation.
+        if ( args->has_append_mode && ialt_exp < line->n_allele )
         {
-            bcf1_t *aline = bcf_sr_get_line(args->files,1);
-            for (j=0; j<args->ncols; j++)
+            while ( ialt_exp++ < line->n_allele )
             {
-                if ( !args->cols[j].setter ) continue;
-                if ( args->cols[j].setter(args,line,&args->cols[j],aline) )
-                    error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+                for (j=0; j<args->ncols; j++)
+                {
+                    if ( args->cols[j].merge_method != MM_APPEND_MISSING ) continue;
+                    if ( args->cols[j].done==1 ) continue;
+                    if ( !args->cols[j].setter ) continue;
+                    int ret = args->cols[j].setter(args,line,&args->cols[j],args->aline_missing);
+                    if ( ret < 0 )
+                        error("fixme: Could not set missing %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+                    if ( ret==0 )
+                        args->cols[j].done = 1;
+                }
             }
-
-            has_overlap = 1;
         }
+        // Flush
+        for (j=0; j<args->ncols; j++)
+        {
+            if ( args->cols[j].done==1 || args->cols[j].merge_method == MM_FIRST ) continue;
+            if ( !args->cols[j].setter ) continue;
+            int ret = args->cols[j].setter(args,line,&args->cols[j],NULL);
+            if ( ret < 0 )
+                error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+        }
+    }
+    return has_overlap;
+}
+static int annotate_from_vcf(args_t *args, bcf1_t *line)
+{
+    if ( !bcf_sr_has_line(args->files,1) ) return 0;
+    int j;
+    bcf1_t *aline = bcf_sr_get_line(args->files,1);
+    for (j=0; j<args->ncols; j++)
+    {
+        if ( !args->cols[j].setter ) continue;
+        if ( args->cols[j].setter(args,line,&args->cols[j],aline) )
+            error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
     }
+    return 1;
+}
+static int annotate_from_self(args_t *args, bcf1_t *line)
+{
+    int j;
+    for (j=0; j<args->ncols; j++)
+    {
+        if ( !args->cols[j].setter ) continue;
+        if ( args->cols[j].setter(args,line,&args->cols[j],NULL) )
+            error("fixme: Could not set %s at %s:%"PRId64"\n", args->cols[j].hdr_key_src,bcf_seqname(args->hdr,line),(int64_t) line->pos+1);
+    }
+    return 0;
+}
+static int annotate_line(args_t *args, bcf1_t *line)
+{
+    args->current_rec = line;
+
+    int i;
+    for (i=0; i<args->nrm; i++)
+        args->rm[i].handler(args, line, &args->rm[i]);
+
+    int has_overlap = 0;
+    if ( args->tgt_idx )
+        has_overlap = annotate_from_regidx(args,line);
+
+    else if ( args->tgts )
+        has_overlap = annotate_from_tab(args,line);
+
+    else if ( args->files->nreaders == 2 )
+        has_overlap = annotate_from_vcf(args,line);
+
+    else if ( args->ncols )
+        has_overlap = annotate_from_self(args,line);
+
     if ( args->set_ids )
     {
         args->tmpks.l = 0;
@@ -3306,6 +3625,8 @@ static void annotate(args_t *args, bcf1_t *line)
         else
             bcf_update_info_flag(args->hdr_out,line,args->mark_sites,NULL,has_overlap?0:1);
     }
+
+    return has_overlap;
 }
 
 static void usage(args_t *args)
@@ -3342,7 +3663,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "       --single-overlaps           Keep memory low by avoiding complexities arising from handling multiple overlapping intervals\n");
     fprintf(bcftools_stderr, "   -x, --remove LIST               List of annotations (e.g. ID,INFO/DP,FORMAT/DP,FILTER) to remove (or keep with \"^\" prefix). See man page for details\n");
     fprintf(bcftools_stderr, "       --threads INT               Number of extra output compression threads [0]\n");
-    fprintf(bcftools_stderr, "       --write-index               Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "   -W, --write-index[=FMT]         Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "Examples:\n");
     fprintf(bcftools_stderr, "   http://samtools.github.io/bcftools/howtos/annotate.html\n");
@@ -3399,11 +3720,11 @@ int main_vcfannotate(int argc, char *argv[])
         {"min-overlap",required_argument,NULL,12},
         {"no-version",no_argument,NULL,8},
         {"force",no_argument,NULL,'f'},
-        {"write-index",no_argument,NULL,13},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "h:H:?o:O:r:R:a:x:c:C:i:e:S:s:I:m:kl:f",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h:H:?o:O:r:R:a:x:c:C:i:e:S:s:I:m:kl:fW::",loptions,NULL)) >= 0)
     {
         switch (c) {
             case 'f': args->force = 1; break;
@@ -3476,7 +3797,10 @@ int main_vcfannotate(int argc, char *argv[])
             case 10 : args->single_overlaps = 1; break;
             case 11 : args->rename_annots = optarg; break;
             case 12 : args->min_overlap_str = optarg; break;
-            case 13 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
         }
@@ -3545,7 +3869,8 @@ int main_vcfannotate(int argc, char *argv[])
                 continue;
             }
         }
-        annotate(args, line);
+        int keep = annotate_line(args, line);
+        if ( args->filter_ext && !args->keep_sites && !keep ) continue;
         if ( bcf_write1(args->out_fh, args->hdr_out, line)!=0 ) error("[%s] Error: failed to write to %s\n", __func__,args->output_fname);
     }
     destroy_data(args);
index 9d60c493cf520f7ff467c02a6685e6ec984d1fa9..22390d0fab096a7e35942d6ec240bcd02e2bb8e6 100644 (file)
@@ -1,6 +1,6 @@
 /* The MIT License
 
-   Copyright (c) 2016-2022 Genome Research Ltd.
+   Copyright (c) 2016-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -29,6 +29,7 @@
 #include <htslib/vcf.h>
 #include <htslib/vcfutils.h>
 #include <htslib/hts_os.h>
+#include <htslib/kbitset.h>
 #include "bcftools.h"
 #include "vcfbuf.h"
 #include "rbuf.h"
@@ -44,7 +45,7 @@ typedef struct
 {
     bcf1_t *rec;
     double af;
-    int af_set:1, filter:1, idx:30;
+    unsigned int af_set:1, filter:1, idx:30;
 }
 vcfrec_t;
 
@@ -61,28 +62,61 @@ typedef struct
 }
 prune_t;
 
+
+#define MARK_OVERLAP 1
+#define MARK_DUP     2
+#define MARK_EXPR    3
+
+#define MARK_MISSING_SCALAR 0   // actual value to use
+#define MARK_MISSING_MAX_DP 1   // max overlap_t.value scaled by INFO/DP
+
+// temporary internal structure for iterative overlap removal by mark_t.expr
 typedef struct
 {
-    int active;
+    double value;       // the sort value
+    int rmme, idx;      // mark for removal, index in vcfbuf_t.rbuf
+    int dp;             // with MARK_MISSING_MAX_DP, INFO/DP is used extrapolate missing QUAL
+    kbitset_t *bset;    // mark which records it overlaps with, given as 0-based indexes to vcfbuf_t.rbuf
+    bcf1_t *rec;
 }
-rmdup_t;
-
+overlap_t;
 typedef struct
 {
-    int active, rid, end;
+    // modes
+    int mode;
+    char *expr;
+
+    // sites marked according to expr, returned to the caller via vcfbuf_get()
+    rbuf_t rbuf;
+    uint8_t *mark;
+    int last;
+
+    // MARK_OVERLAP
+    int overlap_rid, overlap_end;
+
+    // MARK_EXPR
+    int nbuf;
+    overlap_t *buf, **buf_ptr;
+    int missing_expr;       // the value to use when min(QUAL) encounters a missing value
+    float missing_value;    // the default missing value
+    float max_qual;         // with MARK_MISSING_MAX_DP
+    int max_qual_dp;        //
+    int ntmpi;              // temporary int array and the allocated memory
+    int32_t *tmpi;
 }
-overlap_t;
+mark_t;
 
 struct _vcfbuf_t
 {
-    int win, dummy;
+    int win,            // maximum number of sites in the buffer, either number of sites (<0) or bp (<0)
+        dummy;          // the caller maintains the buffer via push/peek/flush
     bcf_hdr_t *hdr;
     vcfrec_t *vcf;
     rbuf_t rbuf;
     ld_t ld;
     prune_t prune;
-    overlap_t overlap;
-    rmdup_t rmdup;
+    mark_t mark;
+    enum { clean, dirty } status;
 };
 
 vcfbuf_t *vcfbuf_init(bcf_hdr_t *hdr, int win)
@@ -90,7 +124,8 @@ vcfbuf_t *vcfbuf_init(bcf_hdr_t *hdr, int win)
     vcfbuf_t *buf = (vcfbuf_t*) calloc(1,sizeof(vcfbuf_t));
     buf->hdr = hdr;
     buf->win = win;
-    buf->overlap.rid = -1;
+    buf->status = clean;
+    buf->mark.overlap_rid = -1;
     int i;
     for (i=0; i<VCFBUF_LD_N; i++) buf->ld.max[i] = HUGE_VAL;
     rbuf_init(&buf->rbuf, 0);
@@ -106,38 +141,119 @@ void vcfbuf_destroy(vcfbuf_t *buf)
     free(buf->prune.farr);
     free(buf->prune.vrec);
     free(buf->prune.ac);
+    free(buf->prune.af_tag);
     free(buf->prune.idx);
+    free(buf->mark.mark);
+    free(buf->mark.expr);
+    for (i=0; i<buf->mark.nbuf; i++) kbs_destroy(buf->mark.buf[i].bset);
+    free(buf->mark.buf);
+    free(buf->mark.buf_ptr);
+    free(buf->mark.tmpi);
     free(buf);
 }
 
-void vcfbuf_set(vcfbuf_t *buf, vcfbuf_opt_t key, void *value)
+int vcfbuf_set(vcfbuf_t *buf, vcfbuf_opt_t key, ...)
 {
-    if ( key==LD_FILTER1 ) { buf->ld.filter1 = *((int*)value); return; }
-    if ( key==LD_RAND_MISSING ) { buf->ld.rand_missing = *((int*)value); return; }
-    if ( key==LD_MAX_R2 ) { buf->ld.max[VCFBUF_LD_IDX_R2] = *((double*)value); return; }
-    if ( key==LD_MAX_LD ) { buf->ld.max[VCFBUF_LD_IDX_LD] = *((double*)value); return; }
-    if ( key==LD_MAX_HD ) { buf->ld.max[VCFBUF_LD_IDX_HD] = *((double*)value); return; }
-
-    if ( key==VCFBUF_DUMMY ) { buf->dummy = *((int*)value); return; }
-    if ( key==VCFBUF_NSITES )
+    va_list args;
+    switch (key)
     {
-        buf->prune.max_sites = *((int*)value);
-        if ( !buf->prune.mode ) buf->prune.mode = PRUNE_MODE_MAX_AF;
-        return;
+        case LD_FILTER1:
+            va_start(args, key);
+            buf->ld.filter1 = va_arg(args,int);
+            va_end(args);
+            return 0;
+
+        case LD_RAND_MISSING:
+            va_start(args, key);
+            buf->ld.rand_missing = va_arg(args,int);
+            va_end(args);
+            return 0;
+
+        case LD_MAX_R2:
+            va_start(args, key);
+            buf->ld.max[VCFBUF_LD_IDX_R2] = va_arg(args,double);
+            va_end(args);
+            return 0;
+
+        case LD_MAX_LD:
+            va_start(args, key);
+            buf->ld.max[VCFBUF_LD_IDX_LD] = va_arg(args,double);
+            va_end(args);
+            return 0;
+
+        case LD_MAX_HD:
+            va_start(args, key);
+            buf->ld.max[VCFBUF_LD_IDX_HD] = va_arg(args,double);
+            va_end(args);
+            return 0;
+
+        case VCFBUF_DUMMY:
+            va_start(args, key);
+            buf->dummy = va_arg(args,int);
+            va_end(args);
+            return 0;
+
+        case PRUNE_NSITES:
+            va_start(args, key);
+            buf->prune.max_sites = va_arg(args,int);
+            if ( !buf->prune.mode ) buf->prune.mode = PRUNE_MODE_MAX_AF;
+            va_end(args);
+            return 0;
+
+        case PRUNE_NSITES_MODE:
+            va_start(args, key);
+            char *mode = va_arg(args,char*);
+            va_end(args);
+            if ( !strcasecmp(mode,"maxAF") ) buf->prune.mode = PRUNE_MODE_MAX_AF;
+            else if ( !strcasecmp(mode,"1st") ) buf->prune.mode = PRUNE_MODE_1ST;
+            else if ( !strcasecmp(mode,"rand") ) buf->prune.mode = PRUNE_MODE_RAND;
+            else error("The mode \"%s\" is not recognised\n",mode);
+            return 0;
+
+        case PRUNE_AF_TAG:
+            va_start(args, key);
+            buf->prune.af_tag = strdup(va_arg(args,char*));
+            va_end(args);
+            return 0;
+
+        case MARK:
+            va_start(args, key);
+            buf->mark.expr = strdup(va_arg(args,char*));
+            if ( !strcasecmp(buf->mark.expr,"overlap") ) buf->mark.mode = MARK_OVERLAP;
+            else if ( !strcasecmp(buf->mark.expr,"dup") ) buf->mark.mode = MARK_DUP;
+            else buf->mark.mode = MARK_EXPR;
+            va_end(args);
+            return 0;
+
+        case MARK_MISSING_EXPR:
+            va_start(args, key);
+            char *expr = va_arg(args,char*);
+            if ( !strcasecmp(expr,"0") )
+            {
+                buf->mark.missing_expr = MARK_MISSING_SCALAR;
+                buf->mark.missing_value = 0;
+            }
+            else if ( !strcasecmp(expr,"DP") )
+            {
+                if ( buf->mark.mode!=MARK_EXPR ) error("Only the combination of --mark 'min(QUAL)' with --missing DP is currently supported\n");
+                buf->mark.missing_expr = MARK_MISSING_MAX_DP;
+            }
+            else
+                error("todo: MARK_MISSING_EXPR=%s\n",expr);
+            va_end(args);
+            return 0;
     }
-    if ( key==VCFBUF_AF_TAG ) { buf->prune.af_tag = *((char**)value); return; }
-    if ( key==VCFBUF_OVERLAP_WIN ) { buf->overlap.active = *((int*)value); return; }
-    if ( key==VCFBUF_RMDUP) { buf->rmdup.active = *((int*)value); return; }
+    return 0;
+}
 
-    if ( key==VCFBUF_NSITES_MODE )
-    {
-        char *mode = *((char**)value);
-        if ( !strcasecmp(mode,"maxAF") ) buf->prune.mode = PRUNE_MODE_MAX_AF;
-        else if ( !strcasecmp(mode,"1st") ) buf->prune.mode = PRUNE_MODE_1ST;
-        else if ( !strcasecmp(mode,"rand") ) buf->prune.mode = PRUNE_MODE_RAND;
-        else error("The mode \"%s\" is not recognised\n",mode);
-        return;
-    }
+void *vcfbuf_get(vcfbuf_t *buf, vcfbuf_opt_t key, ...)
+{
+    va_list args;
+    va_start(args, key);
+    if ( key==MARK )
+        return &buf->mark.last;
+    va_end(args);
+    return NULL;
 }
 
 int vcfbuf_nsites(vcfbuf_t *buf)
@@ -147,8 +263,12 @@ int vcfbuf_nsites(vcfbuf_t *buf)
 
 bcf1_t *vcfbuf_push(vcfbuf_t *buf, bcf1_t *rec)
 {
-    rbuf_expand0(&buf->rbuf, vcfrec_t, buf->rbuf.n+1, buf->vcf);
+    // make sure the caller is using the buffer correctly and calls vcfbuf_flush()
+    // before placing next vcfbuf_push() call
+    assert(buf->status!=dirty);
+    if ( !buf->dummy ) buf->status = dirty;
 
+    rbuf_expand0(&buf->rbuf, vcfrec_t, buf->rbuf.n+1, buf->vcf);
     int i = rbuf_append(&buf->rbuf);
     if ( !buf->vcf[i].rec ) buf->vcf[i].rec = bcf_init1();
 
@@ -163,6 +283,7 @@ bcf1_t *vcfbuf_push(vcfbuf_t *buf, bcf1_t *rec)
 
 bcf1_t *vcfbuf_peek(vcfbuf_t *buf, int idx)
 {
+    buf->status = clean;
     int i = rbuf_kth(&buf->rbuf, idx);
     return i<0 ? NULL : buf->vcf[i].rec;
 }
@@ -195,6 +316,7 @@ static int cmpint_desc(const void *_a, const void *_b)
 
 static void _prune_sites(vcfbuf_t *buf, int flush_all)
 {
+
     int nbuf = flush_all ? buf->rbuf.n : buf->rbuf.n - 1;
 
     int nprune = nbuf - buf->prune.max_sites;
@@ -266,37 +388,75 @@ static void _prune_sites(vcfbuf_t *buf, int flush_all)
         rbuf_remove_kth(&buf->rbuf, vcfrec_t, buf->prune.idx[i], buf->vcf);
 }
 
-static int _rmdup_can_flush(vcfbuf_t *buf, int flush_all)
+static int mark_dup_can_flush_(vcfbuf_t *buf, int flush_all)
 {
-    if ( flush_all ) return 1;
+    int flush = flush_all;
+    mark_t *mark = &buf->mark;
+    if  ( buf->status==dirty )
+    {
+        // a new site was just added by vcfbuf_push()
+        rbuf_expand0(&mark->rbuf, uint8_t, buf->rbuf.n, mark->mark);
+        int i = rbuf_append(&mark->rbuf);
+        mark->mark[i] = 0;
+
+        if ( buf->rbuf.n==1 ) goto flush;
 
-    if ( buf->rbuf.n==1 ) return 0;
+        // there is at least one previous site, check if it's a duplicate
+        int k1 = rbuf_kth(&buf->rbuf, -1);
+        int k2 = rbuf_kth(&buf->rbuf, -2);
+        vcfrec_t *rec1 = &buf->vcf[k1];
+        vcfrec_t *rec2 = &buf->vcf[k2];
 
-    int k1 = rbuf_kth(&buf->rbuf, -1);
-    int k2 = rbuf_kth(&buf->rbuf, -2);
+        int is_dup = 1;
+        if ( rec1->rec->rid!=rec2->rec->rid ) is_dup = 0;
+        else if ( rec1->rec->pos!=rec2->rec->pos ) is_dup = 0;
 
-    vcfrec_t *rec1 = &buf->vcf[k1];
-    vcfrec_t *rec2 = &buf->vcf[k2];
+        if ( is_dup )
+        {
+            // it is, mark the last two sites as duplicates
+            int k1 = rbuf_kth(&mark->rbuf, -1);
+            int k2 = rbuf_kth(&mark->rbuf, -2);
+            mark->mark[k1] = 1;
+            mark->mark[k2] = 1;
+            goto flush;
+        }
 
-    if ( rec1->rec->rid!=rec2->rec->rid ) return 1;
-    if ( rec1->rec->pos!=rec2->rec->pos ) return 1;
+        // the last site is not a duplicate with the previous, all sites but the last one can be flushed
+        flush = 1;
+    }
+    else if ( buf->rbuf.n > 1 ) flush = 1;
 
-    return 0;
+flush:
+    if ( !flush ) return 0;
+
+    int i = rbuf_shift(&mark->rbuf);
+    mark->last = mark->mark[i];
+    return 1;
 }
 
-static int _overlap_can_flush(vcfbuf_t *buf, int flush_all)
+static int mark_overlap_helper_(vcfbuf_t *buf, int flush_all)
 {
-    if ( flush_all ) { buf->overlap.rid = -1; return 1; }
+    if ( buf->status!=dirty ) return flush_all;
 
-    int i = rbuf_last(&buf->rbuf);
-    vcfrec_t *last = &buf->vcf[i];
-    if ( buf->overlap.rid != last->rec->rid ) buf->overlap.end = 0;
+    int flush = flush_all;
+    mark_t *mark = &buf->mark;
 
+    // a new site was just added by vcfbuf_push()
+    buf->status = clean;
+
+    rbuf_expand0(&mark->rbuf, uint8_t, buf->rbuf.n, mark->mark);
+    int i = rbuf_append(&mark->rbuf);
+    mark->mark[i] = 0;
+
+    // determine beg and end of the last record that was just added
+    i = rbuf_last(&buf->rbuf);
+    vcfrec_t *last = &buf->vcf[i];
+    if ( mark->overlap_rid != last->rec->rid ) mark->overlap_end = 0;
     int beg_pos = last->rec->pos;
     int end_pos = last->rec->pos + last->rec->rlen - 1;
 
     // Assuming left-aligned indels. In case it is a deletion, the real variant
-    // starts one base after. If an insertion, the overlap with previous zero length.
+    // starts one base after. If an insertion, the overlap with previous is zero
     int imin = last->rec->rlen;
     for (i=0; i<last->rec->n_allele; i++)
     {
@@ -306,24 +466,175 @@ static int _overlap_can_flush(vcfbuf_t *buf, int flush_all)
         while ( *ref && *alt && nt_to_upper(*ref)==nt_to_upper(*alt) ) { ref++; alt++; }
         if ( imin > ref - last->rec->d.allele[0] ) imin = ref - last->rec->d.allele[0];
     }
-
-    if ( beg_pos <= buf->overlap.end )
+    if ( beg_pos <= mark->overlap_end )
     {
+        // the new site overlaps with the previous
         beg_pos += imin;
         if ( beg_pos > end_pos ) end_pos = beg_pos;
     }
-
     if ( buf->rbuf.n==1 )
     {
-        buf->overlap.rid = last->rec->rid;
-        buf->overlap.end = end_pos;
-        return 0;
+        mark->overlap_rid = last->rec->rid;
+        mark->overlap_end = end_pos;
+        return flush;
+    }
+    if ( beg_pos <= mark->overlap_end )
+    {
+        if ( mark->overlap_end < end_pos ) mark->overlap_end = end_pos;
+        int k1 = rbuf_kth(&mark->rbuf, -1);
+        int k2 = rbuf_kth(&mark->rbuf, -2);
+        mark->mark[k1] = 1;
+        mark->mark[k2] = 1;
+    }
+    else
+    {
+        if ( mark->overlap_end < end_pos ) mark->overlap_end = end_pos;
+        flush = 1;
+    }
+    return flush;
+}
+
+
+static int mark_overlap_can_flush_(vcfbuf_t *buf, int flush_all)
+{
+    int flush = flush_all;
+    if  ( buf->status==dirty ) flush = mark_overlap_helper_(buf,flush_all);
+    else if ( buf->rbuf.n > 1 ) flush = 1;
+    if ( !flush ) return 0;
+
+    mark_t *mark = &buf->mark;
+    int i = rbuf_shift(&mark->rbuf);
+    mark->last = mark->mark[i];
+    return 1;
+}
+
+
+static int records_overlap(bcf1_t *a, bcf1_t *b)
+{
+    if ( a->rid != b->rid ) return 0;
+    if ( a->pos + a->rlen - 1 < b->pos ) return 0;
+    return 1;
+}
+
+static int cmp_overlap_ptr_asc(const void *aptr, const void *bptr)
+{
+    overlap_t *a = *((overlap_t**)aptr);
+    overlap_t *b = *((overlap_t**)bptr);
+    if ( a->value < b->value ) return -1;
+    if ( a->value > b->value ) return 1;
+    return 0;
+}
+static void mark_expr_missing_reset_(vcfbuf_t *buf)
+{
+    buf->mark.max_qual = 0;
+    buf->mark.max_qual_dp = 0;
+}
+static void mark_expr_missing_prep_(vcfbuf_t *buf, overlap_t *olap)
+{
+    int nval = bcf_get_info_int32(buf->hdr,olap->rec,"DP",&buf->mark.tmpi,&buf->mark.ntmpi);
+    if ( nval!=1 ) return;
+
+    olap->dp = buf->mark.tmpi[0];
+    if ( bcf_float_is_missing(olap->rec->qual) ) return;
+    if ( buf->mark.max_qual < olap->rec->qual )
+    {
+        buf->mark.max_qual = olap->rec->qual;
+        buf->mark.max_qual_dp = olap->dp;
     }
-    if ( beg_pos <= buf->overlap.end )
+}
+static void mark_expr_missing_set_(vcfbuf_t *buf, overlap_t *olap)
+{
+    if ( !bcf_float_is_missing(olap->rec->qual) ) return;
+    if ( !buf->mark.max_qual_dp ) return;
+
+    // scale QUAL of the most confident variant in the overlap proportionally to the coverage
+    // and use that to prioritize the records
+    olap->value = buf->mark.max_qual * olap->dp / buf->mark.max_qual_dp;
+}
+static int mark_expr_can_flush_(vcfbuf_t *buf, int flush_all)
+{
+    mark_t *mark = &buf->mark;
+    if ( strcasecmp("min(QUAL)",mark->expr) ) error("Todo; at this time only min(QUAL) is supported\n");
+
+    int flush = flush_all;
+    if  ( buf->status==dirty )
     {
-        if ( buf->overlap.end < end_pos ) buf->overlap.end = end_pos;
-        return 0;
+        flush = mark_overlap_helper_(buf,flush_all);
+        if ( !flush ) return 0;
+
+        if ( mark->missing_expr==MARK_MISSING_MAX_DP ) mark_expr_missing_reset_(buf);
+
+        // init overlaps, each overlap_t structure keeps a list of overlapping records, symmetrical
+        size_t nori = mark->nbuf;
+        hts_resize(overlap_t,  buf->rbuf.n, &mark->nbuf, &mark->buf, HTS_RESIZE_CLEAR);
+        hts_resize(overlap_t*, buf->rbuf.n, &nori, &mark->buf_ptr, HTS_RESIZE_CLEAR);
+        int i;
+        for (i=0; i<buf->rbuf.n; i++)
+        {
+            overlap_t *oi = &mark->buf[i];
+            int j = rbuf_kth(&buf->rbuf, i);
+            assert(j>=0);
+            bcf1_t *rec = buf->vcf[j].rec;
+            assert(rec);
+            oi->rec = rec;
+
+            // todo: other than QUAL values
+            oi->value = bcf_float_is_missing(rec->qual) ? mark->missing_value : rec->qual;
+            if ( mark->missing_expr==MARK_MISSING_MAX_DP ) mark_expr_missing_prep_(buf,oi);
+            if ( oi->bset )
+            {
+                kbs_resize(&oi->bset,buf->rbuf.n);
+                kbs_clear(oi->bset);
+            }
+            else
+                oi->bset = kbs_init(buf->rbuf.n);
+            oi->idx  = i;
+            mark->buf_ptr[i] = oi;
+            mark->mark[oi->idx] = 0;
+        }
+        int nolap = 0;
+        for (i=0; i<buf->rbuf.n; i++)
+        {
+            overlap_t *oi = &mark->buf[i];
+            if ( mark->missing_expr==MARK_MISSING_MAX_DP ) mark_expr_missing_set_(buf,oi);
+            int j;
+            for (j=i+1; j<buf->rbuf.n; j++)
+            {
+                overlap_t *oj = &mark->buf[j];
+                if ( !records_overlap(oi->rec,oj->rec) ) continue;
+                kbs_insert(oi->bset,j);
+                kbs_insert(oj->bset,i);
+                nolap++;
+            }
+        }
+
+        // sort according to the requested criteria, currently only min(QUAL)
+        qsort(mark->buf_ptr,buf->rbuf.n,sizeof(*mark->buf_ptr),cmp_overlap_ptr_asc);   // todo: other than min()
+
+        // go through the list sorted by overlap_t.value, eg QUAL
+        for (i=0; nolap && i<buf->rbuf.n; i++)
+        {
+            kbitset_iter_t itr;
+            overlap_t *oi = mark->buf_ptr[i];
+            kbs_start(&itr);
+            int j;
+            while ((j=kbs_next(oi->bset, &itr)) >= 0)
+            {
+                kbs_delete(oi->bset,j);
+                assert(nolap);
+                assert(kbs_exists(mark->buf[j].bset,oi->idx));
+                kbs_delete(mark->buf[j].bset,oi->idx);
+                nolap--;
+            }
+            j = rbuf_kth(&mark->rbuf,oi->idx);
+            mark->mark[j] = 1;
+        }
     }
+    else if ( buf->rbuf.n > 1 ) flush = 1;
+    if ( !flush ) return 0;
+
+    int i = rbuf_shift(&mark->rbuf);
+    mark->last = mark->mark[i];
     return 1;
 }
 
@@ -331,32 +642,56 @@ bcf1_t *vcfbuf_flush(vcfbuf_t *buf, int flush_all)
 {
     int i,j;
 
+    // nothing to do, no lines in the buffer
     if ( buf->rbuf.n==0 ) return NULL;
-    if ( flush_all || buf->dummy ) goto ret;
-
-    i = rbuf_kth(&buf->rbuf, 0);    // first
-    j = rbuf_last(&buf->rbuf);      // last
 
-    if ( buf->vcf[i].rec->rid != buf->vcf[j].rec->rid ) goto ret;
-    if ( buf->overlap.active && _overlap_can_flush(buf, flush_all) ) goto ret;
-    if ( buf->rmdup.active && _rmdup_can_flush(buf, flush_all) ) goto ret;
+    // dummy mode, always flushing
+    if ( buf->dummy ) goto ret;
 
-    if ( buf->win > 0 )
+    // pruning mode
+    if ( buf->win )
     {
-        if ( buf->rbuf.n <= buf->win ) return NULL;
+        int can_flush = flush_all;
+        i = rbuf_kth(&buf->rbuf, 0);    // first
+        j = rbuf_last(&buf->rbuf);      // last
+        if ( buf->vcf[i].rec->rid != buf->vcf[j].rec->rid ) can_flush = 1;
+        else if ( buf->win > 0 )
+        {
+            if ( buf->rbuf.n > buf->win ) can_flush = 1;
+        }
+        else if ( buf->win < 0 )
+        {
+            if ( !(buf->vcf[i].rec->pos - buf->vcf[j].rec->pos > buf->win) ) can_flush = 1;
+        }
+        buf->status = clean;
+        if ( !can_flush ) return NULL;
+        if ( buf->prune.max_sites && buf->prune.max_sites < buf->rbuf.n ) _prune_sites(buf, flush_all);
         goto ret;
     }
-    else if ( buf->win < 0 )
+
+    // overlaps and duplicates
+    if ( buf->mark.mode )
     {
-        if ( buf->vcf[i].rec->pos - buf->vcf[j].rec->pos > buf->win ) return NULL;
+        int can_flush = 0;
+        if ( buf->mark.mode==MARK_OVERLAP )
+        {
+            if ( mark_overlap_can_flush_(buf,flush_all) ) can_flush = 1;
+        }
+        else if ( buf->mark.mode==MARK_DUP )
+        {
+            if ( mark_dup_can_flush_(buf,flush_all) ) can_flush = 1;
+        }
+        if ( buf->mark.mode==MARK_EXPR )
+        {
+            if ( mark_expr_can_flush_(buf,flush_all) ) can_flush = 1;
+        }
+        buf->status = clean;
+        if ( !can_flush ) return NULL;
         goto ret;
     }
-    else
-        return NULL;
 
 ret:
-    if ( buf->prune.max_sites && buf->prune.max_sites < buf->rbuf.n ) _prune_sites(buf, flush_all);
-
+    buf->status = clean;
     i = rbuf_shift(&buf->rbuf);
     return buf->vcf[i].rec;
 }
index 7b1c40ed9627cf96d1eadd70c3727f9d0a249bf1..b74a5c49b858f400ed5656d381c5b95c5a900f55 100644 (file)
@@ -2,7 +2,7 @@
 
 /* The MIT License
 
-   Copyright (c) 2016-2022 Genome Research Ltd.
+   Copyright (c) 2016-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -31,6 +31,7 @@
 #include <htslib/vcf.h>
 #include <htslib/vcfutils.h>
 #include <htslib/hts_os.h>
+#include <htslib/kbitset.h>
 #include "bcftools.h"
 #include "vcfbuf.h"
 #include "rbuf.h"
@@ -46,7 +47,7 @@ typedef struct
 {
     bcf1_t *rec;
     double af;
-    int af_set:1, filter:1, idx:30;
+    unsigned int af_set:1, filter:1, idx:30;
 }
 vcfrec_t;
 
@@ -63,28 +64,61 @@ typedef struct
 }
 prune_t;
 
+
+#define MARK_OVERLAP 1
+#define MARK_DUP     2
+#define MARK_EXPR    3
+
+#define MARK_MISSING_SCALAR 0   // actual value to use
+#define MARK_MISSING_MAX_DP 1   // max overlap_t.value scaled by INFO/DP
+
+// temporary internal structure for iterative overlap removal by mark_t.expr
 typedef struct
 {
-    int active;
+    double value;       // the sort value
+    int rmme, idx;      // mark for removal, index in vcfbuf_t.rbuf
+    int dp;             // with MARK_MISSING_MAX_DP, INFO/DP is used extrapolate missing QUAL
+    kbitset_t *bset;    // mark which records it overlaps with, given as 0-based indexes to vcfbuf_t.rbuf
+    bcf1_t *rec;
 }
-rmdup_t;
-
+overlap_t;
 typedef struct
 {
-    int active, rid, end;
+    // modes
+    int mode;
+    char *expr;
+
+    // sites marked according to expr, returned to the caller via vcfbuf_get()
+    rbuf_t rbuf;
+    uint8_t *mark;
+    int last;
+
+    // MARK_OVERLAP
+    int overlap_rid, overlap_end;
+
+    // MARK_EXPR
+    int nbuf;
+    overlap_t *buf, **buf_ptr;
+    int missing_expr;       // the value to use when min(QUAL) encounters a missing value
+    float missing_value;    // the default missing value
+    float max_qual;         // with MARK_MISSING_MAX_DP
+    int max_qual_dp;        //
+    int ntmpi;              // temporary int array and the allocated memory
+    int32_t *tmpi;
 }
-overlap_t;
+mark_t;
 
 struct _vcfbuf_t
 {
-    int win, dummy;
+    int win,            // maximum number of sites in the buffer, either number of sites (<0) or bp (<0)
+        dummy;          // the caller maintains the buffer via push/peek/flush
     bcf_hdr_t *hdr;
     vcfrec_t *vcf;
     rbuf_t rbuf;
     ld_t ld;
     prune_t prune;
-    overlap_t overlap;
-    rmdup_t rmdup;
+    mark_t mark;
+    enum { clean, dirty } status;
 };
 
 vcfbuf_t *vcfbuf_init(bcf_hdr_t *hdr, int win)
@@ -92,7 +126,8 @@ vcfbuf_t *vcfbuf_init(bcf_hdr_t *hdr, int win)
     vcfbuf_t *buf = (vcfbuf_t*) calloc(1,sizeof(vcfbuf_t));
     buf->hdr = hdr;
     buf->win = win;
-    buf->overlap.rid = -1;
+    buf->status = clean;
+    buf->mark.overlap_rid = -1;
     int i;
     for (i=0; i<VCFBUF_LD_N; i++) buf->ld.max[i] = HUGE_VAL;
     rbuf_init(&buf->rbuf, 0);
@@ -108,38 +143,119 @@ void vcfbuf_destroy(vcfbuf_t *buf)
     free(buf->prune.farr);
     free(buf->prune.vrec);
     free(buf->prune.ac);
+    free(buf->prune.af_tag);
     free(buf->prune.idx);
+    free(buf->mark.mark);
+    free(buf->mark.expr);
+    for (i=0; i<buf->mark.nbuf; i++) kbs_destroy(buf->mark.buf[i].bset);
+    free(buf->mark.buf);
+    free(buf->mark.buf_ptr);
+    free(buf->mark.tmpi);
     free(buf);
 }
 
-void vcfbuf_set(vcfbuf_t *buf, vcfbuf_opt_t key, void *value)
+int vcfbuf_set(vcfbuf_t *buf, vcfbuf_opt_t key, ...)
 {
-    if ( key==LD_FILTER1 ) { buf->ld.filter1 = *((int*)value); return; }
-    if ( key==LD_RAND_MISSING ) { buf->ld.rand_missing = *((int*)value); return; }
-    if ( key==LD_MAX_R2 ) { buf->ld.max[VCFBUF_LD_IDX_R2] = *((double*)value); return; }
-    if ( key==LD_MAX_LD ) { buf->ld.max[VCFBUF_LD_IDX_LD] = *((double*)value); return; }
-    if ( key==LD_MAX_HD ) { buf->ld.max[VCFBUF_LD_IDX_HD] = *((double*)value); return; }
-
-    if ( key==VCFBUF_DUMMY ) { buf->dummy = *((int*)value); return; }
-    if ( key==VCFBUF_NSITES )
+    va_list args;
+    switch (key)
     {
-        buf->prune.max_sites = *((int*)value);
-        if ( !buf->prune.mode ) buf->prune.mode = PRUNE_MODE_MAX_AF;
-        return;
+        case LD_FILTER1:
+            va_start(args, key);
+            buf->ld.filter1 = va_arg(args,int);
+            va_end(args);
+            return 0;
+
+        case LD_RAND_MISSING:
+            va_start(args, key);
+            buf->ld.rand_missing = va_arg(args,int);
+            va_end(args);
+            return 0;
+
+        case LD_MAX_R2:
+            va_start(args, key);
+            buf->ld.max[VCFBUF_LD_IDX_R2] = va_arg(args,double);
+            va_end(args);
+            return 0;
+
+        case LD_MAX_LD:
+            va_start(args, key);
+            buf->ld.max[VCFBUF_LD_IDX_LD] = va_arg(args,double);
+            va_end(args);
+            return 0;
+
+        case LD_MAX_HD:
+            va_start(args, key);
+            buf->ld.max[VCFBUF_LD_IDX_HD] = va_arg(args,double);
+            va_end(args);
+            return 0;
+
+        case VCFBUF_DUMMY:
+            va_start(args, key);
+            buf->dummy = va_arg(args,int);
+            va_end(args);
+            return 0;
+
+        case PRUNE_NSITES:
+            va_start(args, key);
+            buf->prune.max_sites = va_arg(args,int);
+            if ( !buf->prune.mode ) buf->prune.mode = PRUNE_MODE_MAX_AF;
+            va_end(args);
+            return 0;
+
+        case PRUNE_NSITES_MODE:
+            va_start(args, key);
+            char *mode = va_arg(args,char*);
+            va_end(args);
+            if ( !strcasecmp(mode,"maxAF") ) buf->prune.mode = PRUNE_MODE_MAX_AF;
+            else if ( !strcasecmp(mode,"1st") ) buf->prune.mode = PRUNE_MODE_1ST;
+            else if ( !strcasecmp(mode,"rand") ) buf->prune.mode = PRUNE_MODE_RAND;
+            else error("The mode \"%s\" is not recognised\n",mode);
+            return 0;
+
+        case PRUNE_AF_TAG:
+            va_start(args, key);
+            buf->prune.af_tag = strdup(va_arg(args,char*));
+            va_end(args);
+            return 0;
+
+        case MARK:
+            va_start(args, key);
+            buf->mark.expr = strdup(va_arg(args,char*));
+            if ( !strcasecmp(buf->mark.expr,"overlap") ) buf->mark.mode = MARK_OVERLAP;
+            else if ( !strcasecmp(buf->mark.expr,"dup") ) buf->mark.mode = MARK_DUP;
+            else buf->mark.mode = MARK_EXPR;
+            va_end(args);
+            return 0;
+
+        case MARK_MISSING_EXPR:
+            va_start(args, key);
+            char *expr = va_arg(args,char*);
+            if ( !strcasecmp(expr,"0") )
+            {
+                buf->mark.missing_expr = MARK_MISSING_SCALAR;
+                buf->mark.missing_value = 0;
+            }
+            else if ( !strcasecmp(expr,"DP") )
+            {
+                if ( buf->mark.mode!=MARK_EXPR ) error("Only the combination of --mark 'min(QUAL)' with --missing DP is currently supported\n");
+                buf->mark.missing_expr = MARK_MISSING_MAX_DP;
+            }
+            else
+                error("todo: MARK_MISSING_EXPR=%s\n",expr);
+            va_end(args);
+            return 0;
     }
-    if ( key==VCFBUF_AF_TAG ) { buf->prune.af_tag = *((char**)value); return; }
-    if ( key==VCFBUF_OVERLAP_WIN ) { buf->overlap.active = *((int*)value); return; }
-    if ( key==VCFBUF_RMDUP) { buf->rmdup.active = *((int*)value); return; }
+    return 0;
+}
 
-    if ( key==VCFBUF_NSITES_MODE )
-    {
-        char *mode = *((char**)value);
-        if ( !strcasecmp(mode,"maxAF") ) buf->prune.mode = PRUNE_MODE_MAX_AF;
-        else if ( !strcasecmp(mode,"1st") ) buf->prune.mode = PRUNE_MODE_1ST;
-        else if ( !strcasecmp(mode,"rand") ) buf->prune.mode = PRUNE_MODE_RAND;
-        else error("The mode \"%s\" is not recognised\n",mode);
-        return;
-    }
+void *vcfbuf_get(vcfbuf_t *buf, vcfbuf_opt_t key, ...)
+{
+    va_list args;
+    va_start(args, key);
+    if ( key==MARK )
+        return &buf->mark.last;
+    va_end(args);
+    return NULL;
 }
 
 int vcfbuf_nsites(vcfbuf_t *buf)
@@ -149,8 +265,12 @@ int vcfbuf_nsites(vcfbuf_t *buf)
 
 bcf1_t *vcfbuf_push(vcfbuf_t *buf, bcf1_t *rec)
 {
-    rbuf_expand0(&buf->rbuf, vcfrec_t, buf->rbuf.n+1, buf->vcf);
+    // make sure the caller is using the buffer correctly and calls vcfbuf_flush()
+    // before placing next vcfbuf_push() call
+    assert(buf->status!=dirty);
+    if ( !buf->dummy ) buf->status = dirty;
 
+    rbuf_expand0(&buf->rbuf, vcfrec_t, buf->rbuf.n+1, buf->vcf);
     int i = rbuf_append(&buf->rbuf);
     if ( !buf->vcf[i].rec ) buf->vcf[i].rec = bcf_init1();
 
@@ -165,6 +285,7 @@ bcf1_t *vcfbuf_push(vcfbuf_t *buf, bcf1_t *rec)
 
 bcf1_t *vcfbuf_peek(vcfbuf_t *buf, int idx)
 {
+    buf->status = clean;
     int i = rbuf_kth(&buf->rbuf, idx);
     return i<0 ? NULL : buf->vcf[i].rec;
 }
@@ -197,6 +318,7 @@ static int cmpint_desc(const void *_a, const void *_b)
 
 static void _prune_sites(vcfbuf_t *buf, int flush_all)
 {
+
     int nbuf = flush_all ? buf->rbuf.n : buf->rbuf.n - 1;
 
     int nprune = nbuf - buf->prune.max_sites;
@@ -268,37 +390,75 @@ static void _prune_sites(vcfbuf_t *buf, int flush_all)
         rbuf_remove_kth(&buf->rbuf, vcfrec_t, buf->prune.idx[i], buf->vcf);
 }
 
-static int _rmdup_can_flush(vcfbuf_t *buf, int flush_all)
+static int mark_dup_can_flush_(vcfbuf_t *buf, int flush_all)
 {
-    if ( flush_all ) return 1;
+    int flush = flush_all;
+    mark_t *mark = &buf->mark;
+    if  ( buf->status==dirty )
+    {
+        // a new site was just added by vcfbuf_push()
+        rbuf_expand0(&mark->rbuf, uint8_t, buf->rbuf.n, mark->mark);
+        int i = rbuf_append(&mark->rbuf);
+        mark->mark[i] = 0;
+
+        if ( buf->rbuf.n==1 ) goto flush;
 
-    if ( buf->rbuf.n==1 ) return 0;
+        // there is at least one previous site, check if it's a duplicate
+        int k1 = rbuf_kth(&buf->rbuf, -1);
+        int k2 = rbuf_kth(&buf->rbuf, -2);
+        vcfrec_t *rec1 = &buf->vcf[k1];
+        vcfrec_t *rec2 = &buf->vcf[k2];
 
-    int k1 = rbuf_kth(&buf->rbuf, -1);
-    int k2 = rbuf_kth(&buf->rbuf, -2);
+        int is_dup = 1;
+        if ( rec1->rec->rid!=rec2->rec->rid ) is_dup = 0;
+        else if ( rec1->rec->pos!=rec2->rec->pos ) is_dup = 0;
 
-    vcfrec_t *rec1 = &buf->vcf[k1];
-    vcfrec_t *rec2 = &buf->vcf[k2];
+        if ( is_dup )
+        {
+            // it is, mark the last two sites as duplicates
+            int k1 = rbuf_kth(&mark->rbuf, -1);
+            int k2 = rbuf_kth(&mark->rbuf, -2);
+            mark->mark[k1] = 1;
+            mark->mark[k2] = 1;
+            goto flush;
+        }
 
-    if ( rec1->rec->rid!=rec2->rec->rid ) return 1;
-    if ( rec1->rec->pos!=rec2->rec->pos ) return 1;
+        // the last site is not a duplicate with the previous, all sites but the last one can be flushed
+        flush = 1;
+    }
+    else if ( buf->rbuf.n > 1 ) flush = 1;
 
-    return 0;
+flush:
+    if ( !flush ) return 0;
+
+    int i = rbuf_shift(&mark->rbuf);
+    mark->last = mark->mark[i];
+    return 1;
 }
 
-static int _overlap_can_flush(vcfbuf_t *buf, int flush_all)
+static int mark_overlap_helper_(vcfbuf_t *buf, int flush_all)
 {
-    if ( flush_all ) { buf->overlap.rid = -1; return 1; }
+    if ( buf->status!=dirty ) return flush_all;
 
-    int i = rbuf_last(&buf->rbuf);
-    vcfrec_t *last = &buf->vcf[i];
-    if ( buf->overlap.rid != last->rec->rid ) buf->overlap.end = 0;
+    int flush = flush_all;
+    mark_t *mark = &buf->mark;
 
+    // a new site was just added by vcfbuf_push()
+    buf->status = clean;
+
+    rbuf_expand0(&mark->rbuf, uint8_t, buf->rbuf.n, mark->mark);
+    int i = rbuf_append(&mark->rbuf);
+    mark->mark[i] = 0;
+
+    // determine beg and end of the last record that was just added
+    i = rbuf_last(&buf->rbuf);
+    vcfrec_t *last = &buf->vcf[i];
+    if ( mark->overlap_rid != last->rec->rid ) mark->overlap_end = 0;
     int beg_pos = last->rec->pos;
     int end_pos = last->rec->pos + last->rec->rlen - 1;
 
     // Assuming left-aligned indels. In case it is a deletion, the real variant
-    // starts one base after. If an insertion, the overlap with previous zero length.
+    // starts one base after. If an insertion, the overlap with previous is zero
     int imin = last->rec->rlen;
     for (i=0; i<last->rec->n_allele; i++)
     {
@@ -308,24 +468,175 @@ static int _overlap_can_flush(vcfbuf_t *buf, int flush_all)
         while ( *ref && *alt && nt_to_upper(*ref)==nt_to_upper(*alt) ) { ref++; alt++; }
         if ( imin > ref - last->rec->d.allele[0] ) imin = ref - last->rec->d.allele[0];
     }
-
-    if ( beg_pos <= buf->overlap.end )
+    if ( beg_pos <= mark->overlap_end )
     {
+        // the new site overlaps with the previous
         beg_pos += imin;
         if ( beg_pos > end_pos ) end_pos = beg_pos;
     }
-
     if ( buf->rbuf.n==1 )
     {
-        buf->overlap.rid = last->rec->rid;
-        buf->overlap.end = end_pos;
-        return 0;
+        mark->overlap_rid = last->rec->rid;
+        mark->overlap_end = end_pos;
+        return flush;
+    }
+    if ( beg_pos <= mark->overlap_end )
+    {
+        if ( mark->overlap_end < end_pos ) mark->overlap_end = end_pos;
+        int k1 = rbuf_kth(&mark->rbuf, -1);
+        int k2 = rbuf_kth(&mark->rbuf, -2);
+        mark->mark[k1] = 1;
+        mark->mark[k2] = 1;
+    }
+    else
+    {
+        if ( mark->overlap_end < end_pos ) mark->overlap_end = end_pos;
+        flush = 1;
+    }
+    return flush;
+}
+
+
+static int mark_overlap_can_flush_(vcfbuf_t *buf, int flush_all)
+{
+    int flush = flush_all;
+    if  ( buf->status==dirty ) flush = mark_overlap_helper_(buf,flush_all);
+    else if ( buf->rbuf.n > 1 ) flush = 1;
+    if ( !flush ) return 0;
+
+    mark_t *mark = &buf->mark;
+    int i = rbuf_shift(&mark->rbuf);
+    mark->last = mark->mark[i];
+    return 1;
+}
+
+
+static int records_overlap(bcf1_t *a, bcf1_t *b)
+{
+    if ( a->rid != b->rid ) return 0;
+    if ( a->pos + a->rlen - 1 < b->pos ) return 0;
+    return 1;
+}
+
+static int cmp_overlap_ptr_asc(const void *aptr, const void *bptr)
+{
+    overlap_t *a = *((overlap_t**)aptr);
+    overlap_t *b = *((overlap_t**)bptr);
+    if ( a->value < b->value ) return -1;
+    if ( a->value > b->value ) return 1;
+    return 0;
+}
+static void mark_expr_missing_reset_(vcfbuf_t *buf)
+{
+    buf->mark.max_qual = 0;
+    buf->mark.max_qual_dp = 0;
+}
+static void mark_expr_missing_prep_(vcfbuf_t *buf, overlap_t *olap)
+{
+    int nval = bcf_get_info_int32(buf->hdr,olap->rec,"DP",&buf->mark.tmpi,&buf->mark.ntmpi);
+    if ( nval!=1 ) return;
+
+    olap->dp = buf->mark.tmpi[0];
+    if ( bcf_float_is_missing(olap->rec->qual) ) return;
+    if ( buf->mark.max_qual < olap->rec->qual )
+    {
+        buf->mark.max_qual = olap->rec->qual;
+        buf->mark.max_qual_dp = olap->dp;
     }
-    if ( beg_pos <= buf->overlap.end )
+}
+static void mark_expr_missing_set_(vcfbuf_t *buf, overlap_t *olap)
+{
+    if ( !bcf_float_is_missing(olap->rec->qual) ) return;
+    if ( !buf->mark.max_qual_dp ) return;
+
+    // scale QUAL of the most confident variant in the overlap proportionally to the coverage
+    // and use that to prioritize the records
+    olap->value = buf->mark.max_qual * olap->dp / buf->mark.max_qual_dp;
+}
+static int mark_expr_can_flush_(vcfbuf_t *buf, int flush_all)
+{
+    mark_t *mark = &buf->mark;
+    if ( strcasecmp("min(QUAL)",mark->expr) ) error("Todo; at this time only min(QUAL) is supported\n");
+
+    int flush = flush_all;
+    if  ( buf->status==dirty )
     {
-        if ( buf->overlap.end < end_pos ) buf->overlap.end = end_pos;
-        return 0;
+        flush = mark_overlap_helper_(buf,flush_all);
+        if ( !flush ) return 0;
+
+        if ( mark->missing_expr==MARK_MISSING_MAX_DP ) mark_expr_missing_reset_(buf);
+
+        // init overlaps, each overlap_t structure keeps a list of overlapping records, symmetrical
+        size_t nori = mark->nbuf;
+        hts_resize(overlap_t,  buf->rbuf.n, &mark->nbuf, &mark->buf, HTS_RESIZE_CLEAR);
+        hts_resize(overlap_t*, buf->rbuf.n, &nori, &mark->buf_ptr, HTS_RESIZE_CLEAR);
+        int i;
+        for (i=0; i<buf->rbuf.n; i++)
+        {
+            overlap_t *oi = &mark->buf[i];
+            int j = rbuf_kth(&buf->rbuf, i);
+            assert(j>=0);
+            bcf1_t *rec = buf->vcf[j].rec;
+            assert(rec);
+            oi->rec = rec;
+
+            // todo: other than QUAL values
+            oi->value = bcf_float_is_missing(rec->qual) ? mark->missing_value : rec->qual;
+            if ( mark->missing_expr==MARK_MISSING_MAX_DP ) mark_expr_missing_prep_(buf,oi);
+            if ( oi->bset )
+            {
+                kbs_resize(&oi->bset,buf->rbuf.n);
+                kbs_clear(oi->bset);
+            }
+            else
+                oi->bset = kbs_init(buf->rbuf.n);
+            oi->idx  = i;
+            mark->buf_ptr[i] = oi;
+            mark->mark[oi->idx] = 0;
+        }
+        int nolap = 0;
+        for (i=0; i<buf->rbuf.n; i++)
+        {
+            overlap_t *oi = &mark->buf[i];
+            if ( mark->missing_expr==MARK_MISSING_MAX_DP ) mark_expr_missing_set_(buf,oi);
+            int j;
+            for (j=i+1; j<buf->rbuf.n; j++)
+            {
+                overlap_t *oj = &mark->buf[j];
+                if ( !records_overlap(oi->rec,oj->rec) ) continue;
+                kbs_insert(oi->bset,j);
+                kbs_insert(oj->bset,i);
+                nolap++;
+            }
+        }
+
+        // sort according to the requested criteria, currently only min(QUAL)
+        qsort(mark->buf_ptr,buf->rbuf.n,sizeof(*mark->buf_ptr),cmp_overlap_ptr_asc);   // todo: other than min()
+
+        // go through the list sorted by overlap_t.value, eg QUAL
+        for (i=0; nolap && i<buf->rbuf.n; i++)
+        {
+            kbitset_iter_t itr;
+            overlap_t *oi = mark->buf_ptr[i];
+            kbs_start(&itr);
+            int j;
+            while ((j=kbs_next(oi->bset, &itr)) >= 0)
+            {
+                kbs_delete(oi->bset,j);
+                assert(nolap);
+                assert(kbs_exists(mark->buf[j].bset,oi->idx));
+                kbs_delete(mark->buf[j].bset,oi->idx);
+                nolap--;
+            }
+            j = rbuf_kth(&mark->rbuf,oi->idx);
+            mark->mark[j] = 1;
+        }
     }
+    else if ( buf->rbuf.n > 1 ) flush = 1;
+    if ( !flush ) return 0;
+
+    int i = rbuf_shift(&mark->rbuf);
+    mark->last = mark->mark[i];
     return 1;
 }
 
@@ -333,32 +644,56 @@ bcf1_t *vcfbuf_flush(vcfbuf_t *buf, int flush_all)
 {
     int i,j;
 
+    // nothing to do, no lines in the buffer
     if ( buf->rbuf.n==0 ) return NULL;
-    if ( flush_all || buf->dummy ) goto ret;
-
-    i = rbuf_kth(&buf->rbuf, 0);    // first
-    j = rbuf_last(&buf->rbuf);      // last
 
-    if ( buf->vcf[i].rec->rid != buf->vcf[j].rec->rid ) goto ret;
-    if ( buf->overlap.active && _overlap_can_flush(buf, flush_all) ) goto ret;
-    if ( buf->rmdup.active && _rmdup_can_flush(buf, flush_all) ) goto ret;
+    // dummy mode, always flushing
+    if ( buf->dummy ) goto ret;
 
-    if ( buf->win > 0 )
+    // pruning mode
+    if ( buf->win )
     {
-        if ( buf->rbuf.n <= buf->win ) return NULL;
+        int can_flush = flush_all;
+        i = rbuf_kth(&buf->rbuf, 0);    // first
+        j = rbuf_last(&buf->rbuf);      // last
+        if ( buf->vcf[i].rec->rid != buf->vcf[j].rec->rid ) can_flush = 1;
+        else if ( buf->win > 0 )
+        {
+            if ( buf->rbuf.n > buf->win ) can_flush = 1;
+        }
+        else if ( buf->win < 0 )
+        {
+            if ( !(buf->vcf[i].rec->pos - buf->vcf[j].rec->pos > buf->win) ) can_flush = 1;
+        }
+        buf->status = clean;
+        if ( !can_flush ) return NULL;
+        if ( buf->prune.max_sites && buf->prune.max_sites < buf->rbuf.n ) _prune_sites(buf, flush_all);
         goto ret;
     }
-    else if ( buf->win < 0 )
+
+    // overlaps and duplicates
+    if ( buf->mark.mode )
     {
-        if ( buf->vcf[i].rec->pos - buf->vcf[j].rec->pos > buf->win ) return NULL;
+        int can_flush = 0;
+        if ( buf->mark.mode==MARK_OVERLAP )
+        {
+            if ( mark_overlap_can_flush_(buf,flush_all) ) can_flush = 1;
+        }
+        else if ( buf->mark.mode==MARK_DUP )
+        {
+            if ( mark_dup_can_flush_(buf,flush_all) ) can_flush = 1;
+        }
+        if ( buf->mark.mode==MARK_EXPR )
+        {
+            if ( mark_expr_can_flush_(buf,flush_all) ) can_flush = 1;
+        }
+        buf->status = clean;
+        if ( !can_flush ) return NULL;
         goto ret;
     }
-    else
-        return NULL;
 
 ret:
-    if ( buf->prune.max_sites && buf->prune.max_sites < buf->rbuf.n ) _prune_sites(buf, flush_all);
-
+    buf->status = clean;
     i = rbuf_shift(&buf->rbuf);
     return buf->vcf[i].rec;
 }
index 878fd10440ca59f917656f305c8cdf9dd8e5916b..96d7115c2fda6a187b44781bcd76bd0849c98336 100644 (file)
@@ -1,6 +1,6 @@
 /* The MIT License
 
-   Copyright (c) 2017-2022 Genome Research Ltd.
+   Copyright (c) 2017-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -38,13 +38,27 @@ typedef struct _vcfbuf_t vcfbuf_t;
 // Modes of operation
 typedef enum
 {
-    VCFBUF_DUMMY,           // the caller maintains the buffer via push/peek/flush, nothing is removed by vcfbuf
-
-    VCFBUF_OVERLAP_WIN,     // keep only overlapping variants in the window
-    VCFBUF_RMDUP,           // remove duplicate sites (completely)
-    VCFBUF_NSITES,          // leave at max this many sites in the window
-    VCFBUF_NSITES_MODE,     // one of: maxAF (keep sites with max AF), 1st (sites that come first), rand (pick randomly)
-    VCFBUF_AF_TAG,          // use this INFO tag with VCFBUF_NSITES
+    VCFBUF_DUMMY,           // int {0,1}, the caller maintains the buffer via push/peek/flush, nothing is removed by vcfbuf
+
+    // pruning
+    PRUNE_NSITES,           // int, leave max this many sites in the window
+    PRUNE_NSITES_MODE,      // char *, maxAF (keep sites with max AF), 1st (sites that come first), rand (pick randomly)
+    PRUNE_AF_TAG,           // char *, use this INFO/AF tag with VCFBUF_NSITES
+
+    // duplicates and overlaps
+    MARK,                   // w: char *, resolve overlaps by preferentially removing sites according to EXPR:
+                            //      min(QUAL) .. remove sites with lowest QUAL until overlaps are resolved
+                            //      overlap   .. select all overlapping sites
+                            //      dup       .. select duplicate sites
+                            // r: use as
+                            //      while ( (rec=vcfbuf_flush(buf,flush_all)) )
+                            //      {
+                            //          int is_marked = vcfbuf_get_val(buf,int,MARK);
+                            //          if ( is_marked ) do_something(rec);
+                            //      }
+    MARK_MISSING_EXPR,      // char *, what to do when missing value are encountered with min(QUAL)
+                            //      0   .. set to 0 (the default)
+                            //      DP  .. scale max quality in the window proportionally to INFO/DP
 
     // LD related options
     LD_RAND_MISSING,        // randomize rather than ignore missing genotypes
@@ -55,8 +69,23 @@ typedef enum
 }
 vcfbuf_opt_t;
 
-#define vcfbuf_set_opt(buf,type,key,value) { type tmp = value; vcfbuf_set(buf, key, (void*)&tmp); }
-void vcfbuf_set(vcfbuf_t *buf, vcfbuf_opt_t key, void *value);
+
+/**
+ *  vcfbuf_set() - set various options, see the vcfbuf_opt_t keys for the complete list
+ *
+ *  Returns 0 if the call succeeded, or negative number on error.
+ */
+int vcfbuf_set(vcfbuf_t *buf, vcfbuf_opt_t key, ...);   // returns 0 on success
+
+/**
+ *  vcfbuf_get()     - get various options, see the vcfbuf_opt_t keys
+ *  vcfbuf_get_val() - wrapper for `vcfbuf_get()` to return typed value
+ *
+ *  The former returns pointer to the memory area populated by the requested setting,
+ *  its type can be inferred from the vcfbuf_opt_t documentation.
+ */
+void *vcfbuf_get(vcfbuf_t *buf, vcfbuf_opt_t key, ...);
+#define vcfbuf_get_val(buf,type,key) (*(type*)vcfbuf_get(buf, key))
 
 
 /*
@@ -67,7 +96,9 @@ vcfbuf_t *vcfbuf_init(bcf_hdr_t *hdr, int win);
 void vcfbuf_destroy(vcfbuf_t *buf);
 
 /*
- *  vcfbuf_push() - push a new site for analysis
+ *  vcfbuf_push() - push a new site for analysis.
+ *
+ *  Note that vcfbuf_flush() or vcfbuf_peek() must be called before next site is pushed.
  */
 bcf1_t *vcfbuf_push(vcfbuf_t *buf, bcf1_t *rec);
 
@@ -86,6 +117,8 @@ bcf1_t *vcfbuf_remove(vcfbuf_t *buf, int idx);
 /*
  *  vcfbuf_flush() - returns the next record or NULL, depending on the mode of operation and
  *      the content of the buffer
+ *
+ *  @flush_all: 1 if no more vcfbuf_push() calls will follow, 0 otherwise
  */
 bcf1_t *vcfbuf_flush(vcfbuf_t *buf, int flush_all);
 
index d2f6e2c5fcf37106bac220938086335c521e6326..13e516f83879956a2487757e3dd0d1b90efa57c1 100644 (file)
@@ -1,6 +1,6 @@
 /*  vcfcall.c -- SNP/indel variant calling from VCF/BCF.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -397,15 +397,16 @@ static int tgt_parse(const char *line, char **chr_beg, char **chr_end, uint32_t
     while ( *ss )
     {
         se = ss;
-        while ( *se && *se!=',' ) se++;
+        while ( *se && *se!=',' && !isspace(*se) ) se++;
         als->n++;
         als->allele = (char**)realloc(als->allele,als->n*sizeof(*als->allele));
         als->allele[als->n-1] = (char*)malloc(se-ss+1);
         memcpy(als->allele[als->n-1],ss,se-ss);
         als->allele[als->n-1][se-ss] = 0;
         ss = se+1;
-        if ( !*se ) break;
+        if ( !*se || isspace(*se) ) break;
     }
+    if ( als->n<2 ) error("Unable to parse the -T file; expected CHROM\\tPOS\\tREF,ALT with -C alleles but found instead:\n\t%s\n",line);
     return 0;
 }
 static void tgt_free(void *payload)
@@ -695,7 +696,10 @@ static void init_data(args_t *args)
     }
 
     if ( args->aux.flag & CALL_CONSTR_ALLELES )
+    {
         args->vcfbuf = vcfbuf_init(args->aux.hdr, 0);
+        vcfbuf_set(args->vcfbuf,VCFBUF_DUMMY,1);
+    }
 
     char wmode[8];
     set_wmode(wmode,args->output_type,args->output_fname,args->clevel);
@@ -717,7 +721,9 @@ static void init_data(args_t *args)
 
     if (args->record_cmd_line) bcf_hdr_append_version(args->aux.hdr, args->argc, args->argv, "bcftools_call");
     if ( bcf_hdr_write(args->out_fh, args->aux.hdr)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(args->out_fh,args->aux.hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( init_index2(args->out_fh,args->aux.hdr,args->output_fname,
+                     &args->index_fn, args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
 
     if ( args->flag&CF_INS_MISSED ) init_missed_line(args);
 }
@@ -910,6 +916,7 @@ static void usage(args_t *args)
     fprintf(stderr, "\n");
     fprintf(stderr, "Input/output options:\n");
     fprintf(stderr, "   -A, --keep-alts                 Keep all possible alternate alleles at variant sites\n");
+    fprintf(stderr, "   -*, --keep-unseen-allele        Keep the unobserved allele <*> or <NON_REF>\n");
     fprintf(stderr, "   -a, --annotate LIST             Optional tags to output (lowercase allowed); '?' to list available tags\n");
     fprintf(stderr, "   -F, --prior-freqs AN,AC         Use prior allele frequencies, determined from these pre-filled tags\n");
     fprintf(stderr, "   -G, --group-samples FILE|-      Group samples by population (file with \"sample\\tgroup\") or \"-\" for single-sample calling.\n");
@@ -920,7 +927,7 @@ static void usage(args_t *args)
     fprintf(stderr, "   -M, --keep-masked-ref           Keep sites with masked reference allele (REF=N)\n");
     fprintf(stderr, "   -V, --skip-variants TYPE        Skip indels/snps\n");
     fprintf(stderr, "   -v, --variants-only             Output variant sites only\n");
-    fprintf(stderr, "       --write-index               Automatically index the output files [off]\n");
+    fprintf(stderr, "   -W, --write-index[=FMT]         Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     fprintf(stderr, "Consensus/variant calling options:\n");
     fprintf(stderr, "   -c, --consensus-caller          The original calling method (conflicts with -m)\n");
@@ -937,7 +944,7 @@ static void usage(args_t *args)
     // todo (and more)
     // fprintf(stderr, "\nContrast calling and association test options:\n");
     // fprintf(stderr, "       -1 INT    number of group-1 samples [0]\n");
-    // fprintf(stderr, "       -C FLOAT  posterior constrast for LRT<FLOAT and P(ref|D)<0.5 [%g]\n", args->aux.min_lrt);
+    // fprintf(stderr, "       -C FLOAT  posterior contrast for LRT<FLOAT and P(ref|D)<0.5 [%g]\n", args->aux.min_lrt);
     // fprintf(stderr, "       -U INT    number of permutations for association testing (effective with -1) [0]\n");
     // fprintf(stderr, "       -X FLOAT  only perform permutations for P(chi^2)<FLOAT [%g]\n", args->aux.min_perm_p);
     fprintf(stderr, "\n");
@@ -987,6 +994,7 @@ int main_vcfcall(int argc, char *argv[])
         {"targets-file",required_argument,NULL,'T'},
         {"threads",required_argument,NULL,9},
         {"keep-alts",no_argument,NULL,'A'},
+        {"keep-unseen-allele",no_argument,NULL,'*'},
         {"insert-missed",no_argument,NULL,'i'},
         {"skip-Ns",no_argument,NULL,'N'},            // now the new default
         {"keep-masked-refs",no_argument,NULL,'M'},
@@ -1003,12 +1011,12 @@ int main_vcfcall(int argc, char *argv[])
         {"chromosome-X",no_argument,NULL,'X'},
         {"chromosome-Y",no_argument,NULL,'Y'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,10},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
 
     char *tmp = NULL;
-    while ((c = getopt_long(argc, argv, "h?o:O:r:R:s:S:t:T:ANMV:vcmp:C:n:P:f:a:ig:XYF:G:", loptions, NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h?o:O:r:R:s:S:t:T:A*NMV:vcmp:C:n:P:f:a:ig:XYF:G:W::", loptions, NULL)) >= 0)
     {
         switch (c)
         {
@@ -1026,6 +1034,7 @@ int main_vcfcall(int argc, char *argv[])
             case 'M': args.flag &= ~CF_ACGT_ONLY; break;     // keep sites where REF is N
             case 'N': args.flag |= CF_ACGT_ONLY; break;      // omit sites where first base in REF is N (the new default)
             case 'A': args.aux.flag |= CALL_KEEPALT; break;
+            case '*': args.aux.flag |= CALL_KEEP_UNSEEN; break;
             case 'c': args.flag |= CF_CCALL; break;          // the original EM based calling method
             case 'i': args.flag |= CF_INS_MISSED; break;
             case 'v': args.aux.flag |= CALL_VARONLY; break;
@@ -1090,7 +1099,10 @@ int main_vcfcall(int argc, char *argv[])
                 args.regions_overlap = parse_overlap_option(optarg);
                 if ( args.regions_overlap < 0 ) error("Could not parse: --regions-overlap %s\n",optarg);
                 break;
-            case  10: args.write_index = 1; break;
+            case 'W':
+                if (!(args.write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             default: usage(&args);
         }
     }
index a955342f47c00a8f20ec677a634d3da17f612073..0da344ef1968393dad5d2ebc48f30f2107d5d7aa 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  vcfcall.c -- SNP/indel variant calling from VCF/BCF.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -399,15 +399,16 @@ static int tgt_parse(const char *line, char **chr_beg, char **chr_end, uint32_t
     while ( *ss )
     {
         se = ss;
-        while ( *se && *se!=',' ) se++;
+        while ( *se && *se!=',' && !isspace(*se) ) se++;
         als->n++;
         als->allele = (char**)realloc(als->allele,als->n*sizeof(*als->allele));
         als->allele[als->n-1] = (char*)malloc(se-ss+1);
         memcpy(als->allele[als->n-1],ss,se-ss);
         als->allele[als->n-1][se-ss] = 0;
         ss = se+1;
-        if ( !*se ) break;
+        if ( !*se || isspace(*se) ) break;
     }
+    if ( als->n<2 ) error("Unable to parse the -T file; expected CHROM\\tPOS\\tREF,ALT with -C alleles but found instead:\n\t%s\n",line);
     return 0;
 }
 static void tgt_free(void *payload)
@@ -697,7 +698,10 @@ static void init_data(args_t *args)
     }
 
     if ( args->aux.flag & CALL_CONSTR_ALLELES )
+    {
         args->vcfbuf = vcfbuf_init(args->aux.hdr, 0);
+        vcfbuf_set(args->vcfbuf,VCFBUF_DUMMY,1);
+    }
 
     char wmode[8];
     set_wmode(wmode,args->output_type,args->output_fname,args->clevel);
@@ -719,7 +723,9 @@ static void init_data(args_t *args)
 
     if (args->record_cmd_line) bcf_hdr_append_version(args->aux.hdr, args->argc, args->argv, "bcftools_call");
     if ( bcf_hdr_write(args->out_fh, args->aux.hdr)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(args->out_fh,args->aux.hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( init_index2(args->out_fh,args->aux.hdr,args->output_fname,
+                     &args->index_fn, args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
 
     if ( args->flag&CF_INS_MISSED ) init_missed_line(args);
 }
@@ -912,6 +918,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "Input/output options:\n");
     fprintf(bcftools_stderr, "   -A, --keep-alts                 Keep all possible alternate alleles at variant sites\n");
+    fprintf(bcftools_stderr, "   -*, --keep-unseen-allele        Keep the unobserved allele <*> or <NON_REF>\n");
     fprintf(bcftools_stderr, "   -a, --annotate LIST             Optional tags to output (lowercase allowed); '?' to list available tags\n");
     fprintf(bcftools_stderr, "   -F, --prior-freqs AN,AC         Use prior allele frequencies, determined from these pre-filled tags\n");
     fprintf(bcftools_stderr, "   -G, --group-samples FILE|-      Group samples by population (file with \"sample\\tgroup\") or \"-\" for single-sample calling.\n");
@@ -922,7 +929,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "   -M, --keep-masked-ref           Keep sites with masked reference allele (REF=N)\n");
     fprintf(bcftools_stderr, "   -V, --skip-variants TYPE        Skip indels/snps\n");
     fprintf(bcftools_stderr, "   -v, --variants-only             Output variant sites only\n");
-    fprintf(bcftools_stderr, "       --write-index               Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "   -W, --write-index[=FMT]         Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "Consensus/variant calling options:\n");
     fprintf(bcftools_stderr, "   -c, --consensus-caller          The original calling method (conflicts with -m)\n");
@@ -939,7 +946,7 @@ static void usage(args_t *args)
     // todo (and more)
     // fprintf(bcftools_stderr, "\nContrast calling and association test options:\n");
     // fprintf(bcftools_stderr, "       -1 INT    number of group-1 samples [0]\n");
-    // fprintf(bcftools_stderr, "       -C FLOAT  posterior constrast for LRT<FLOAT and P(ref|D)<0.5 [%g]\n", args->aux.min_lrt);
+    // fprintf(bcftools_stderr, "       -C FLOAT  posterior contrast for LRT<FLOAT and P(ref|D)<0.5 [%g]\n", args->aux.min_lrt);
     // fprintf(bcftools_stderr, "       -U INT    number of permutations for association testing (effective with -1) [0]\n");
     // fprintf(bcftools_stderr, "       -X FLOAT  only perform permutations for P(chi^2)<FLOAT [%g]\n", args->aux.min_perm_p);
     fprintf(bcftools_stderr, "\n");
@@ -989,6 +996,7 @@ int main_vcfcall(int argc, char *argv[])
         {"targets-file",required_argument,NULL,'T'},
         {"threads",required_argument,NULL,9},
         {"keep-alts",no_argument,NULL,'A'},
+        {"keep-unseen-allele",no_argument,NULL,'*'},
         {"insert-missed",no_argument,NULL,'i'},
         {"skip-Ns",no_argument,NULL,'N'},            // now the new default
         {"keep-masked-refs",no_argument,NULL,'M'},
@@ -1005,12 +1013,12 @@ int main_vcfcall(int argc, char *argv[])
         {"chromosome-X",no_argument,NULL,'X'},
         {"chromosome-Y",no_argument,NULL,'Y'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,10},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
 
     char *tmp = NULL;
-    while ((c = getopt_long(argc, argv, "h?o:O:r:R:s:S:t:T:ANMV:vcmp:C:n:P:f:a:ig:XYF:G:", loptions, NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h?o:O:r:R:s:S:t:T:A*NMV:vcmp:C:n:P:f:a:ig:XYF:G:W::", loptions, NULL)) >= 0)
     {
         switch (c)
         {
@@ -1028,6 +1036,7 @@ int main_vcfcall(int argc, char *argv[])
             case 'M': args.flag &= ~CF_ACGT_ONLY; break;     // keep sites where REF is N
             case 'N': args.flag |= CF_ACGT_ONLY; break;      // omit sites where first base in REF is N (the new default)
             case 'A': args.aux.flag |= CALL_KEEPALT; break;
+            case '*': args.aux.flag |= CALL_KEEP_UNSEEN; break;
             case 'c': args.flag |= CF_CCALL; break;          // the original EM based calling method
             case 'i': args.flag |= CF_INS_MISSED; break;
             case 'v': args.aux.flag |= CALL_VARONLY; break;
@@ -1092,7 +1101,10 @@ int main_vcfcall(int argc, char *argv[])
                 args.regions_overlap = parse_overlap_option(optarg);
                 if ( args.regions_overlap < 0 ) error("Could not parse: --regions-overlap %s\n",optarg);
                 break;
-            case  10: args.write_index = 1; break;
+            case 'W':
+                if (!(args.write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             default: usage(&args);
         }
     }
index 0302261d5f13c551328c650e831fa4216b3659cd..e970b043b9a34f9a0c28e123eebd9e4e6921a8ce 100644 (file)
@@ -41,6 +41,7 @@
 #include <htslib/kstring.h>
 #include <htslib/kfunc.h>
 #include <htslib/khash_str2int.h>
+#include <htslib/hts_defs.h>
 #include "bcftools.h"
 #include "HMM.h"
 #include "rbuf.h"
@@ -105,7 +106,7 @@ typedef struct _args_t
 }
 args_t;
 
-FILE *open_file(char **fname, const char *mode, const char *fmt, ...);
+FILE *open_file(char **fname, const char *mode, const char *fmt, ...) HTS_FORMAT(HTS_PRINTF_FMT, 3, 4);
 
 static inline void hmm2cn_state(int nstates, int i, int *a, int *b)
 {
@@ -879,7 +880,7 @@ static int update_sample_args(args_t *args, sample_t *smpl, int ismpl)
 
     /*
         A noisy CN2 band is hard to distinguish from two CN3 bands which are
-        close to each other. Set a treshold on the minimum separation based
+        close to each other. Set a threshold on the minimum separation based
         on the BAF deviation at p=0.95
     */
     baf_dev2 /= norm_cn3;
@@ -1297,7 +1298,7 @@ int main_vcfcnv(int argc, char *argv[])
         {0,0,0,0}
     };
     char *tmp = NULL;
-    while ((c = getopt_long(argc, argv, "h?r:R:t:T:s:o:p:l:T:c:b:P:x:e:O:W:f:a:L:d:k:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "h?r:R:t:T:s:o:p:l:T:c:b:P:x:e:O:W::f:a:L:d:k:",loptions,NULL)) >= 0) {
         switch (c) {
             case 'L': 
                 args->lrr_smooth_win = strtol(optarg,&tmp,10);
index fd2e3bba1e9066a9b9d4c9cca9984d291238171c..d6ee421216671ac43de1d160dac7bc7b1a31e435 100644 (file)
@@ -43,6 +43,7 @@
 #include <htslib/kstring.h>
 #include <htslib/kfunc.h>
 #include <htslib/khash_str2int.h>
+#include <htslib/hts_defs.h>
 #include "bcftools.h"
 #include "HMM.h"
 #include "rbuf.h"
@@ -107,7 +108,7 @@ typedef struct _args_t
 }
 args_t;
 
-FILE *open_file(char **fname, const char *mode, const char *fmt, ...);
+FILE *open_file(char **fname, const char *mode, const char *fmt, ...) HTS_FORMAT(HTS_PRINTF_FMT, 3, 4);
 
 static inline void hmm2cn_state(int nstates, int i, int *a, int *b)
 {
@@ -881,7 +882,7 @@ static int update_sample_args(args_t *args, sample_t *smpl, int ismpl)
 
     /*
         A noisy CN2 band is hard to distinguish from two CN3 bands which are
-        close to each other. Set a treshold on the minimum separation based
+        close to each other. Set a threshold on the minimum separation based
         on the BAF deviation at p=0.95
     */
     baf_dev2 /= norm_cn3;
@@ -1299,7 +1300,7 @@ int main_vcfcnv(int argc, char *argv[])
         {0,0,0,0}
     };
     char *tmp = NULL;
-    while ((c = getopt_long(argc, argv, "h?r:R:t:T:s:o:p:l:T:c:b:P:x:e:O:W:f:a:L:d:k:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "h?r:R:t:T:s:o:p:l:T:c:b:P:x:e:O:W::f:a:L:d:k:",loptions,NULL)) >= 0) {
         switch (c) {
             case 'L': 
                 args->lrr_smooth_win = strtol(optarg,&tmp,10);
index 8e25cc5902113753b161da3d24845b184f027996..232b3ae390d87a0d0139549eaff577604453e7f3 100644 (file)
@@ -29,6 +29,7 @@ THE SOFTWARE.  */
 #include <assert.h>
 #include <errno.h>
 #include <math.h>
+#include <stdint.h>
 #include <inttypes.h>
 #include <htslib/vcf.h>
 #include <htslib/synced_bcf_reader.h>
@@ -36,6 +37,7 @@ THE SOFTWARE.  */
 #include <htslib/bgzf.h>
 #include <htslib/tbx.h> // for hts_get_bgzfp()
 #include <htslib/thread_pool.h>
+#include <htslib/hts_endian.h>
 #include <sys/time.h>
 #include "bcftools.h"
 
@@ -157,7 +159,9 @@ static void init_data(args_t *args)
         hts_set_opt(args->out_fh, HTS_OPT_THREAD_POOL, args->tpool);
     }
     if ( bcf_hdr_write(args->out_fh, args->out_hdr)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(args->out_fh,args->out_hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( init_index2(args->out_fh,args->out_hdr,args->output_fname,
+                     &args->index_fn, args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
 
     if ( args->allow_overlaps )
     {
@@ -781,7 +785,7 @@ static void _check_hrecs(const bcf_hdr_t *hdr0, const bcf_hdr_t *hdr, char *fnam
     for (j=0; j<hdr0->nhrec; j++)
     {
         bcf_hrec_t *hrec0 = hdr0->hrec[j];
-        if ( hrec0->type!=BCF_HL_FLT && hrec0->type!=BCF_HL_INFO && hrec0->type!=BCF_HL_FMT && hrec0->type!=BCF_HL_CTG ) continue;    // skip fiels w/o IDX
+        if ( hrec0->type!=BCF_HL_FLT && hrec0->type!=BCF_HL_INFO && hrec0->type!=BCF_HL_FMT && hrec0->type!=BCF_HL_CTG ) continue;    // skip fields w/o IDX
         int itag = bcf_hrec_find_key(hrec0, "ID");
         bcf_hrec_t *hrec = bcf_hdr_get_hrec(hdr, hrec0->type, "ID", hrec0->vals[itag], NULL);
 
@@ -887,19 +891,20 @@ static void naive_concat(args_t *args)
         int nskip;
         if ( type.format==bcf )
         {
-            uint8_t magic[5];
-            if ( bgzf_read(fp, magic, 5) != 5 ) error("\nFailed to read the BCF header in %s\n", args->fnames[i]);
+            const size_t magic_len = 5 + 4; // "Magic" string + header length
+            uint8_t magic[magic_len];
+            if ( bgzf_read(fp, magic, magic_len) != magic_len ) error("\nFailed to read the BCF header in %s\n", args->fnames[i]);
+            // First five bytes are the "Magic" string
             if (strncmp((char*)magic, "BCF\2\2", 5) != 0) error("\nInvalid BCF magic string in %s\n", args->fnames[i]);
-
-            if ( bgzf_read(fp, &tmp.l, 4) != 4 ) error("\nFailed to read the BCF header in %s\n", args->fnames[i]);
+            // Next four are the header length (little-endian)
+            tmp.l = le_to_u32(magic + 5);
             hts_expand(char,tmp.l,tmp.m,tmp.s);
             if ( bgzf_read(fp, tmp.s, tmp.l) != tmp.l ) error("\nFailed to read the BCF header in %s\n", args->fnames[i]);
 
             // write only the first header
             if ( i==0 )
             {
-                if ( bgzf_write(bgzf_out, "BCF\2\2", 5) !=5 ) error("\nFailed to write %d bytes to %s\n", 5,args->output_fname);
-                if ( bgzf_write(bgzf_out, &tmp.l, 4) !=4 ) error("\nFailed to write %d bytes to %s\n", 4,args->output_fname);
+                if ( bgzf_write(bgzf_out, magic, magic_len) != magic_len ) error("\nFailed to write %zu bytes to %s\n", magic_len,args->output_fname);
                 if ( bgzf_write(bgzf_out, tmp.s, tmp.l) != tmp.l) error("\nFailed to write %"PRId64" bytes to %s\n", (uint64_t)tmp.l,args->output_fname);
             }
             nskip = fp->block_offset;
@@ -982,7 +987,7 @@ static void usage(args_t *args)
     fprintf(stderr, "       --regions-overlap 0|1|2    Include if POS in the region (0), record overlaps (1), variant overlaps (2) [1]\n");
     fprintf(stderr, "       --threads INT              Use multithreading with <int> worker threads [0]\n");
     fprintf(stderr, "   -v, --verbose 0|1              Set verbosity level [1]\n");
-    fprintf(stderr, "       --write-index              Automatically index the output files [off]\n");
+    fprintf(stderr, "   -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     exit(1);
 }
@@ -1021,12 +1026,12 @@ int main_vcfconcat(int argc, char *argv[])
         {"file-list",required_argument,NULL,'f'},
         {"min-PQ",required_argument,NULL,'q'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,13},
+        {"write-index",optional_argument,NULL,'W'},
         {"drop-genotypes",no_argument,NULL,'G'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "h:?o:O:f:alq:Dd:Gr:R:cnv:",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h:?o:O:f:alq:Dd:Gr:R:cnv:W::",loptions,NULL)) >= 0)
     {
         switch (c) {
             case 'c': args->compact_PS = 1; break;
@@ -1076,7 +1081,10 @@ int main_vcfconcat(int argc, char *argv[])
                       args->verbose = strtol(optarg, &tmp, 0);
                       if ( *tmp || args->verbose<0 || args->verbose>1 ) error("Error: currently only --verbose 0 or --verbose 1 is supported\n");
                       break;
-            case 13 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'h':
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
@@ -1104,6 +1112,7 @@ int main_vcfconcat(int argc, char *argv[])
     if ( args->regions_list && !args->allow_overlaps ) error("The -r/-R option is supported only with -a\n");
     if ( args->naive_concat )
     {
+        if ( args->write_index ) error("Error: cannot --write-index in the %s mode\n",args->naive_concat_trust_headers?"--naive-force":"--naive");
         if ( args->allow_overlaps ) error("The option --naive cannot be combined with --allow-overlaps\n");
         if ( args->phased_concat ) error("The option --naive cannot be combined with --ligate\n");
         if ( args->sites_only ) error("The option --naive cannot be combined with --drop-genotypes\n");
index 0d3b3943cc1c8d6cfcba2120ca785616dd213d41..d238dc0455d3655d7887189140aac2b87801df7a 100644 (file)
@@ -31,6 +31,7 @@ THE SOFTWARE.  */
 #include <assert.h>
 #include <errno.h>
 #include <math.h>
+#include <stdint.h>
 #include <inttypes.h>
 #include <htslib/vcf.h>
 #include <htslib/synced_bcf_reader.h>
@@ -38,6 +39,7 @@ THE SOFTWARE.  */
 #include <htslib/bgzf.h>
 #include <htslib/tbx.h> // for hts_get_bgzfp()
 #include <htslib/thread_pool.h>
+#include <htslib/hts_endian.h>
 #include <sys/time.h>
 #include "bcftools.h"
 
@@ -159,7 +161,9 @@ static void init_data(args_t *args)
         hts_set_opt(args->out_fh, HTS_OPT_THREAD_POOL, args->tpool);
     }
     if ( bcf_hdr_write(args->out_fh, args->out_hdr)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(args->out_fh,args->out_hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( init_index2(args->out_fh,args->out_hdr,args->output_fname,
+                     &args->index_fn, args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
 
     if ( args->allow_overlaps )
     {
@@ -783,7 +787,7 @@ static void _check_hrecs(const bcf_hdr_t *hdr0, const bcf_hdr_t *hdr, char *fnam
     for (j=0; j<hdr0->nhrec; j++)
     {
         bcf_hrec_t *hrec0 = hdr0->hrec[j];
-        if ( hrec0->type!=BCF_HL_FLT && hrec0->type!=BCF_HL_INFO && hrec0->type!=BCF_HL_FMT && hrec0->type!=BCF_HL_CTG ) continue;    // skip fiels w/o IDX
+        if ( hrec0->type!=BCF_HL_FLT && hrec0->type!=BCF_HL_INFO && hrec0->type!=BCF_HL_FMT && hrec0->type!=BCF_HL_CTG ) continue;    // skip fields w/o IDX
         int itag = bcf_hrec_find_key(hrec0, "ID");
         bcf_hrec_t *hrec = bcf_hdr_get_hrec(hdr, hrec0->type, "ID", hrec0->vals[itag], NULL);
 
@@ -889,19 +893,20 @@ static void naive_concat(args_t *args)
         int nskip;
         if ( type.format==bcf )
         {
-            uint8_t magic[5];
-            if ( bgzf_read(fp, magic, 5) != 5 ) error("\nFailed to read the BCF header in %s\n", args->fnames[i]);
+            const size_t magic_len = 5 + 4; // "Magic" string + header length
+            uint8_t magic[magic_len];
+            if ( bgzf_read(fp, magic, magic_len) != magic_len ) error("\nFailed to read the BCF header in %s\n", args->fnames[i]);
+            // First five bytes are the "Magic" string
             if (strncmp((char*)magic, "BCF\2\2", 5) != 0) error("\nInvalid BCF magic string in %s\n", args->fnames[i]);
-
-            if ( bgzf_read(fp, &tmp.l, 4) != 4 ) error("\nFailed to read the BCF header in %s\n", args->fnames[i]);
+            // Next four are the header length (little-endian)
+            tmp.l = le_to_u32(magic + 5);
             hts_expand(char,tmp.l,tmp.m,tmp.s);
             if ( bgzf_read(fp, tmp.s, tmp.l) != tmp.l ) error("\nFailed to read the BCF header in %s\n", args->fnames[i]);
 
             // write only the first header
             if ( i==0 )
             {
-                if ( bgzf_write(bgzf_out, "BCF\2\2", 5) !=5 ) error("\nFailed to write %d bytes to %s\n", 5,args->output_fname);
-                if ( bgzf_write(bgzf_out, &tmp.l, 4) !=4 ) error("\nFailed to write %d bytes to %s\n", 4,args->output_fname);
+                if ( bgzf_write(bgzf_out, magic, magic_len) != magic_len ) error("\nFailed to write %zu bytes to %s\n", magic_len,args->output_fname);
                 if ( bgzf_write(bgzf_out, tmp.s, tmp.l) != tmp.l) error("\nFailed to write %"PRId64" bytes to %s\n", (uint64_t)tmp.l,args->output_fname);
             }
             nskip = fp->block_offset;
@@ -984,7 +989,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "       --regions-overlap 0|1|2    Include if POS in the region (0), record overlaps (1), variant overlaps (2) [1]\n");
     fprintf(bcftools_stderr, "       --threads INT              Use multithreading with <int> worker threads [0]\n");
     fprintf(bcftools_stderr, "   -v, --verbose 0|1              Set verbosity level [1]\n");
-    fprintf(bcftools_stderr, "       --write-index              Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "   -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     bcftools_exit(1);
 }
@@ -1023,12 +1028,12 @@ int main_vcfconcat(int argc, char *argv[])
         {"file-list",required_argument,NULL,'f'},
         {"min-PQ",required_argument,NULL,'q'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,13},
+        {"write-index",optional_argument,NULL,'W'},
         {"drop-genotypes",no_argument,NULL,'G'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "h:?o:O:f:alq:Dd:Gr:R:cnv:",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h:?o:O:f:alq:Dd:Gr:R:cnv:W::",loptions,NULL)) >= 0)
     {
         switch (c) {
             case 'c': args->compact_PS = 1; break;
@@ -1078,7 +1083,10 @@ int main_vcfconcat(int argc, char *argv[])
                       args->verbose = strtol(optarg, &tmp, 0);
                       if ( *tmp || args->verbose<0 || args->verbose>1 ) error("Error: currently only --verbose 0 or --verbose 1 is supported\n");
                       break;
-            case 13 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'h':
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
@@ -1106,6 +1114,7 @@ int main_vcfconcat(int argc, char *argv[])
     if ( args->regions_list && !args->allow_overlaps ) error("The -r/-R option is supported only with -a\n");
     if ( args->naive_concat )
     {
+        if ( args->write_index ) error("Error: cannot --write-index in the %s mode\n",args->naive_concat_trust_headers?"--naive-force":"--naive");
         if ( args->allow_overlaps ) error("The option --naive cannot be combined with --allow-overlaps\n");
         if ( args->phased_concat ) error("The option --naive cannot be combined with --ligate\n");
         if ( args->sites_only ) error("The option --naive cannot be combined with --drop-genotypes\n");
index 76c4a325adba2143f0b59f0b0d123e4b2000c4e3..f75085aabb422263d18683cea7509a1167c12055 100644 (file)
@@ -31,6 +31,7 @@ THE SOFTWARE.  */
 #include <errno.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <stdint.h>
 #include <inttypes.h>
 #include <htslib/faidx.h>
 #include <htslib/vcf.h>
@@ -38,6 +39,7 @@ THE SOFTWARE.  */
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
 #include <htslib/kseq.h>
+#include <htslib/hts_endian.h>
 #include "bcftools.h"
 #include "filter.h"
 #include "convert.h"
@@ -209,7 +211,10 @@ static int _set_chrom_pos_ref_alt(tsv_t *tsv, bcf1_t *rec, void *usr)
     {
         long end = strtol(se+1,&ss,10);
         if ( ss==se+1 ) return -1;
-        bcf_update_info_int32(args->header, rec, "END", &end, 1);
+        if (end < 1 || end > INT32_MAX)
+            return -1;
+        int32_t e = end; // bcf_update_info_int32 needs an int32_t pointer
+        bcf_update_info_int32(args->header, rec, "END", &e, 1);
     }
 
     rec->rid = rid;
@@ -490,7 +495,9 @@ static void gensample_to_vcf(args_t *args)
     if ( out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->outfname, strerror(errno));
     if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
     if ( bcf_hdr_write(out_fh,args->header)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
     bcf1_t *rec = bcf_init();
 
     nsamples -= 2;
@@ -634,7 +641,9 @@ static void haplegendsample_to_vcf(args_t *args)
     if ( out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->outfname, strerror(errno));
     if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
     if ( bcf_hdr_write(out_fh,args->header)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
     bcf1_t *rec = bcf_init();
 
     args->gts = (int32_t *) malloc(sizeof(int32_t)*nsamples*2);
@@ -786,7 +795,9 @@ static void hapsample_to_vcf(args_t *args)
     if ( out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->outfname, strerror(errno));
     if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
     if ( bcf_hdr_write(out_fh,args->header)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
     bcf1_t *rec = bcf_init();
 
     nsamples -= 2;
@@ -1389,7 +1400,9 @@ static void tsv_to_vcf(args_t *args)
     if ( out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->outfname, strerror(errno));
     if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
     if ( bcf_hdr_write(out_fh,args->header)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
 
     tsv_t *tsv = tsv_init(args->columns ? args->columns : "ID,CHROM,POS,AA");
     if ( tsv_register(tsv, "CHROM", tsv_setter_chrom, args->header) < 0 ) error("Expected CHROM column\n");
@@ -1468,7 +1481,9 @@ static void vcf_to_vcf(args_t *args)
 
     bcf_hdr_t *hdr = bcf_sr_get_header(args->files,0);
     if ( bcf_hdr_write(out_fh,hdr)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
 
     while ( bcf_sr_next_line(args->files) )
     {
@@ -1510,7 +1525,9 @@ static void gvcf_to_vcf(args_t *args)
     bcf_hdr_t *hdr = bcf_sr_get_header(args->files,0);
     if (args->record_cmd_line) bcf_hdr_append_version(hdr, args->argc, args->argv, "bcftools_convert");
     if ( bcf_hdr_write(out_fh,hdr)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,hdr,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,hdr,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
 
     int32_t *itmp = NULL, nitmp = 0;
 
@@ -1608,7 +1625,7 @@ static void usage(void)
     fprintf(stderr, "   -o, --output FILE              Output file name [stdout]\n");
     fprintf(stderr, "   -O, --output-type u|b|v|z[0-9] u/b: un/compressed BCF, v/z: un/compressed VCF, 0-9: compression level [v]\n");
     fprintf(stderr, "       --threads INT              Use multithreading with INT worker threads [0]\n");
-    fprintf(stderr, "       --write-index              Automatically index the output files [off]\n");
+    fprintf(stderr, "   -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     fprintf(stderr, "GEN/SAMPLE conversion (input/output from IMPUTE2):\n");
     fprintf(stderr, "   -G, --gensample2vcf ...        <PREFIX>|<GEN-FILE>,<SAMPLE-FILE>\n");
@@ -1702,11 +1719,11 @@ int main_vcfconvert(int argc, char *argv[])
         {"fasta-ref",required_argument,NULL,'f'},
         {"no-version",no_argument,NULL,10},
         {"keep-duplicates",no_argument,NULL,12},
-        {"write-index",no_argument,NULL,16},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "?h:r:R:s:S:t:T:i:e:g:G:o:O:c:f:H:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "?h:r:R:s:S:t:T:i:e:g:G:o:O:c:f:H:W::",loptions,NULL)) >= 0) {
         switch (c) {
             case 'e':
                 if ( args->filter_str ) error("Error: only one -i or -e expression can be given, and they cannot be combined\n");
@@ -1731,7 +1748,10 @@ int main_vcfconvert(int argc, char *argv[])
             case  7 : args->convert_func = vcf_to_hapsample; args->outfname = optarg; break;
             case  8 : error("The --chrom option has been deprecated, please use --3N6 instead\n"); break;
             case 15 : args->gen_3N6 = 1; break;
-            case 16 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'H': args->convert_func = haplegendsample_to_vcf; args->infname = optarg; break;
             case 'f': args->ref_fname = optarg; break;
             case 'c': args->columns = optarg; break;
index 16bb3be685acbdfc15c4dc49f6d4b8f7ef294153..f8921bf6381625f4bc2ed5012da66fc707eec5fe 100644 (file)
@@ -33,6 +33,7 @@ THE SOFTWARE.  */
 #include <errno.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <stdint.h>
 #include <inttypes.h>
 #include <htslib/faidx.h>
 #include <htslib/vcf.h>
@@ -40,6 +41,7 @@ THE SOFTWARE.  */
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
 #include <htslib/kseq.h>
+#include <htslib/hts_endian.h>
 #include "bcftools.h"
 #include "filter.h"
 #include "convert.h"
@@ -211,7 +213,10 @@ static int _set_chrom_pos_ref_alt(tsv_t *tsv, bcf1_t *rec, void *usr)
     {
         long end = strtol(se+1,&ss,10);
         if ( ss==se+1 ) return -1;
-        bcf_update_info_int32(args->header, rec, "END", &end, 1);
+        if (end < 1 || end > INT32_MAX)
+            return -1;
+        int32_t e = end; // bcf_update_info_int32 needs an int32_t pointer
+        bcf_update_info_int32(args->header, rec, "END", &e, 1);
     }
 
     rec->rid = rid;
@@ -492,7 +497,9 @@ static void gensample_to_vcf(args_t *args)
     if ( out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->outfname, strerror(errno));
     if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
     if ( bcf_hdr_write(out_fh,args->header)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
     bcf1_t *rec = bcf_init();
 
     nsamples -= 2;
@@ -636,7 +643,9 @@ static void haplegendsample_to_vcf(args_t *args)
     if ( out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->outfname, strerror(errno));
     if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
     if ( bcf_hdr_write(out_fh,args->header)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
     bcf1_t *rec = bcf_init();
 
     args->gts = (int32_t *) malloc(sizeof(int32_t)*nsamples*2);
@@ -788,7 +797,9 @@ static void hapsample_to_vcf(args_t *args)
     if ( out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->outfname, strerror(errno));
     if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
     if ( bcf_hdr_write(out_fh,args->header)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
     bcf1_t *rec = bcf_init();
 
     nsamples -= 2;
@@ -1391,7 +1402,9 @@ static void tsv_to_vcf(args_t *args)
     if ( out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->outfname, strerror(errno));
     if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
     if ( bcf_hdr_write(out_fh,args->header)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
 
     tsv_t *tsv = tsv_init(args->columns ? args->columns : "ID,CHROM,POS,AA");
     if ( tsv_register(tsv, "CHROM", tsv_setter_chrom, args->header) < 0 ) error("Expected CHROM column\n");
@@ -1470,7 +1483,9 @@ static void vcf_to_vcf(args_t *args)
 
     bcf_hdr_t *hdr = bcf_sr_get_header(args->files,0);
     if ( bcf_hdr_write(out_fh,hdr)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,args->header,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,args->header,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
 
     while ( bcf_sr_next_line(args->files) )
     {
@@ -1512,7 +1527,9 @@ static void gvcf_to_vcf(args_t *args)
     bcf_hdr_t *hdr = bcf_sr_get_header(args->files,0);
     if (args->record_cmd_line) bcf_hdr_append_version(hdr, args->argc, args->argv, "bcftools_convert");
     if ( bcf_hdr_write(out_fh,hdr)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->outfname);
-    if ( args->write_index && init_index(out_fh,hdr,args->outfname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->outfname);
+    if ( init_index2(out_fh,hdr,args->outfname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->outfname);
 
     int32_t *itmp = NULL, nitmp = 0;
 
@@ -1610,7 +1627,7 @@ static void usage(void)
     fprintf(bcftools_stderr, "   -o, --output FILE              Output file name [bcftools_stdout]\n");
     fprintf(bcftools_stderr, "   -O, --output-type u|b|v|z[0-9] u/b: un/compressed BCF, v/z: un/compressed VCF, 0-9: compression level [v]\n");
     fprintf(bcftools_stderr, "       --threads INT              Use multithreading with INT worker threads [0]\n");
-    fprintf(bcftools_stderr, "       --write-index              Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "   -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "GEN/SAMPLE conversion (input/output from IMPUTE2):\n");
     fprintf(bcftools_stderr, "   -G, --gensample2vcf ...        <PREFIX>|<GEN-FILE>,<SAMPLE-FILE>\n");
@@ -1704,11 +1721,11 @@ int main_vcfconvert(int argc, char *argv[])
         {"fasta-ref",required_argument,NULL,'f'},
         {"no-version",no_argument,NULL,10},
         {"keep-duplicates",no_argument,NULL,12},
-        {"write-index",no_argument,NULL,16},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "?h:r:R:s:S:t:T:i:e:g:G:o:O:c:f:H:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "?h:r:R:s:S:t:T:i:e:g:G:o:O:c:f:H:W::",loptions,NULL)) >= 0) {
         switch (c) {
             case 'e':
                 if ( args->filter_str ) error("Error: only one -i or -e expression can be given, and they cannot be combined\n");
@@ -1733,7 +1750,10 @@ int main_vcfconvert(int argc, char *argv[])
             case  7 : args->convert_func = vcf_to_hapsample; args->outfname = optarg; break;
             case  8 : error("The --chrom option has been deprecated, please use --3N6 instead\n"); break;
             case 15 : args->gen_3N6 = 1; break;
-            case 16 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'H': args->convert_func = haplegendsample_to_vcf; args->infname = optarg; break;
             case 'f': args->ref_fname = optarg; break;
             case 'c': args->columns = optarg; break;
index 8665409d158dde9a394bd48f58cde79038a7b220..52d4f9455567ba1f2e39b415bbc1dbd5820a1c77 100644 (file)
@@ -493,7 +493,7 @@ static void usage(args_t *args)
     fprintf(stderr, "    -T, --targets-file FILE        Similar to -R but streams rather than index-jumps\n");
     fprintf(stderr, "        --targets-overlap 0|1|2    Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n");
     fprintf(stderr, "        --threads INT              Use multithreading with <int> worker threads [0]\n");
-    fprintf(stderr, "        --write-index              Automatically index the output files [off]\n");
+    fprintf(stderr, "    -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     exit(1);
 }
@@ -536,11 +536,11 @@ int main_vcffilter(int argc, char *argv[])
         {"SnpGap",required_argument,NULL,'g'},
         {"IndelGap",required_argument,NULL,'G'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,12},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "e:i:t:T:r:R:h?s:m:M:o:O:g:G:S:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "e:i:t:T:r:R:h?s:m:M:o:O:g:G:S:W::",loptions,NULL)) >= 0) {
         switch (c) {
             case 'g':
                 args->snp_gap = strtol(optarg,&tmp,10);
@@ -629,7 +629,10 @@ int main_vcffilter(int argc, char *argv[])
                 else if ( !strcasecmp(optarg,"2") ) args->mask_overlap = 2;
                 else error("Could not parse: --mask-overlap %s\n",optarg);
                 break;
-            case  12 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'h':
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
@@ -677,7 +680,9 @@ int main_vcffilter(int argc, char *argv[])
 
     init_data(args);
     if ( bcf_hdr_write(args->out_fh, args->hdr)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(args->out_fh,args->hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( init_index2(args->out_fh,args->hdr,args->output_fname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
     while ( bcf_sr_next_line(args->files) )
     {
         bcf1_t *line = bcf_sr_get_line(args->files, 0);
index 6d17151e9ce8416a571f35d886bd445edea5cbcf..c240f799d2eaae3fdbb13325fb25e2799ecb91b6 100644 (file)
@@ -495,7 +495,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "    -T, --targets-file FILE        Similar to -R but streams rather than index-jumps\n");
     fprintf(bcftools_stderr, "        --targets-overlap 0|1|2    Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n");
     fprintf(bcftools_stderr, "        --threads INT              Use multithreading with <int> worker threads [0]\n");
-    fprintf(bcftools_stderr, "        --write-index              Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "    -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     bcftools_exit(1);
 }
@@ -538,11 +538,11 @@ int main_vcffilter(int argc, char *argv[])
         {"SnpGap",required_argument,NULL,'g'},
         {"IndelGap",required_argument,NULL,'G'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,12},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "e:i:t:T:r:R:h?s:m:M:o:O:g:G:S:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "e:i:t:T:r:R:h?s:m:M:o:O:g:G:S:W::",loptions,NULL)) >= 0) {
         switch (c) {
             case 'g':
                 args->snp_gap = strtol(optarg,&tmp,10);
@@ -631,7 +631,10 @@ int main_vcffilter(int argc, char *argv[])
                 else if ( !strcasecmp(optarg,"2") ) args->mask_overlap = 2;
                 else error("Could not parse: --mask-overlap %s\n",optarg);
                 break;
-            case  12 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'h':
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
@@ -679,7 +682,9 @@ int main_vcffilter(int argc, char *argv[])
 
     init_data(args);
     if ( bcf_hdr_write(args->out_fh, args->hdr)!=0 ) error("[%s] Error: cannot write the header to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(args->out_fh,args->hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( init_index2(args->out_fh,args->hdr,args->output_fname,&args->index_fn,
+                     args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
     while ( bcf_sr_next_line(args->files) )
     {
         bcf1_t *line = bcf_sr_get_line(args->files, 0);
index 561be62a5c4679ad381eb8a6388a0f03f9dfec68..be886db3458cc8157071f267ec7fd58b7cfb9b54 100644 (file)
@@ -1,6 +1,6 @@
 /*  vcfgtcheck.c -- Check sample identity.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -39,11 +39,17 @@ THE SOFTWARE.  */
 #include <htslib/vcfutils.h>
 #include <htslib/kbitset.h>
 #include <htslib/hts_os.h>
+#include <htslib/bgzf.h>
 #include <inttypes.h>
 #include <sys/time.h>
 #include "bcftools.h"
 #include "extsort.h"
 //#include "hclust.h"
+#include "filter.h"
+
+// Logic of the filters: include or exclude sites which match the filters?
+#define FLT_INCLUDE 1
+#define FLT_EXCLUDE 2
 
 typedef struct
 {
@@ -56,20 +62,22 @@ typedef struct
     bcf_srs_t *files;           // first reader is the query VCF - single sample normally or multi-sample for cross-check
     bcf_hdr_t *gt_hdr, *qry_hdr; // VCF with genotypes to compare against and the query VCF
     char *cwd, **argv, *gt_samples, *qry_samples, *regions, *targets, *qry_fname, *gt_fname, *pair_samples;
-    int argc, gt_samples_is_file, qry_samples_is_file, regions_is_file, targets_is_file, pair_samples_is_file;
-    int regions_overlap, targets_overlap;
+    char *output_fname;
+    int argc, gt_samples_is_file, qry_samples_is_file, regions_is_file, targets_is_file, pair_samples_is_file, output_type;;
+    int regions_overlap, targets_overlap, clevel;
     int qry_use_GT,gt_use_GT, nqry_smpl,ngt_smpl, *qry_smpl,*gt_smpl;
     int nused[2][2];
     double *pdiff, *qry_prob, *gt_prob;
-    uint32_t *ndiff,*ncnt,ncmp, npairs;
+    uint32_t *ndiff,*ncnt,*nmatch,ncmp, npairs;
     int32_t *qry_arr,*gt_arr, nqry_arr,ngt_arr;
     uint8_t *qry_dsg, *gt_dsg;
     pair_t *pairs;
     double *hwe_prob, dsg2prob[8][3], pl2prob[256];
     double min_inter_err, max_intra_err;
-    int all_sites, hom_only, ntop, cross_check, calc_hwe_prob, sort_by_hwe, dry_run, use_PLs;
-    FILE *fp;
-    unsigned int nskip_no_match, nskip_not_ba, nskip_mono, nskip_no_data, nskip_dip_GT, nskip_dip_PL;
+    int all_sites, hom_only, ntop, cross_check, calc_hwe_prob, sort_by_hwe, dry_run, gt_err;
+    BGZF *out_fh;
+    unsigned int nskip_no_match, nskip_not_ba, nskip_mono, nskip_no_data, nskip_dip_GT, nskip_dip_PL, nskip_filter;
+    kstring_t kstr;
 
     // for --distinctive-sites
     double distinctive_sites;
@@ -77,6 +85,11 @@ typedef struct
     size_t diff_sites_size;
     extsort_t *es;
     char *es_tmp_prefix, *es_max_mem;
+
+    // include or exclude sites which match the filters
+    filter_t *qry_filter, *gt_filter;
+    char *qry_filter_str, *gt_filter_str;
+    int qry_filter_logic, gt_filter_logic;       // FLT_INCLUDE or FLT_EXCLUDE
 }
 args_t;
 
@@ -94,15 +107,17 @@ static void set_cwd(args_t *args)
     }
     assert(buf);
 }
-static void print_header(args_t *args, FILE *fp)
+static void print_header(args_t *args)
 {
-    fprintf(fp, "# This file was produced by bcftools (%s+htslib-%s), the command line was:\n", bcftools_version(), hts_version());
-    fprintf(fp, "# \t bcftools %s ", args->argv[0]);
+    args->kstr.l = 0;
+    ksprintf(&args->kstr, "# This file was produced by bcftools (%s+htslib-%s), the command line was:\n", bcftools_version(), hts_version());
+    ksprintf(&args->kstr, "# \t bcftools %s ", args->argv[0]);
     int i;
     for (i=1; i<args->argc; i++)
-        fprintf(fp, " %s",args->argv[i]);
-    fprintf(fp, "\n# and the working directory was:\n");
-    fprintf(fp, "# \t %s\n#\n", args->cwd);
+        ksprintf(&args->kstr, " %s",args->argv[i]);
+    ksprintf(&args->kstr, "\n# and the working directory was:\n");
+    ksprintf(&args->kstr, "# \t %s\n#\n", args->cwd);
+    if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
 }
 
 static int cmp_int(const void *_a, const void *_b)
@@ -126,7 +141,7 @@ static int cmp_pair(const void *_a, const void *_b)
 
 typedef struct
 {
-    uint32_t ndiff,rid,pos,rand; // rand is to shuffle sites with the same ndiff from across all chromosoms
+    uint32_t ndiff,rid,pos,rand; // rand is to shuffle sites with the same ndiff from across all chromosomes
     unsigned long kbs_dat[1];
 }
 diff_sites_t;
@@ -262,6 +277,9 @@ static void init_data(args_t *args)
         if ( !bcf_hdr_nsamples(args->gt_hdr) ) error("No samples in %s?\n", args->gt_fname);
     }
 
+    if ( args->gt_hdr && args->gt_filter_str ) args->gt_filter = filter_init(args->gt_hdr, args->gt_filter_str);
+    if ( args->qry_hdr && args->qry_filter_str ) args->qry_filter = filter_init(args->qry_hdr, args->qry_filter_str);
+
     // Determine whether GT or PL will be used
     if ( args->qry_use_GT==-1 ) // not set by -u, qry uses PL by default
     {
@@ -371,56 +389,76 @@ static void init_data(args_t *args)
         args->qry_dsg = (uint8_t*) malloc(args->nqry_smpl);
         args->gt_dsg  = args->cross_check ? args->qry_dsg : (uint8_t*) malloc(args->ngt_smpl);
     }
-    if ( args->use_PLs )
+    if ( args->gt_err )
     {
         args->pdiff = (double*) calloc(args->npairs,sizeof(*args->pdiff));      // log probability of pair samples being the same
         args->qry_prob = (double*) malloc(3*args->nqry_smpl*sizeof(*args->qry_prob));
         args->gt_prob  = args->cross_check ? args->qry_prob : (double*) malloc(3*args->ngt_smpl*sizeof(*args->gt_prob));
 
+        // Convert genotypes to genotype likelihoods given by -E, the probability of reading one allele incorrectly. In this
+        // simple model we have:
+        //     - probability of reading an allele incorrectly, eg. 0 as 1 or 1 as 0
+        //         P(0|1) = P(1|0) = e
+        //     - probability of genotype G={00,01,11} being correct given observed dosage {0,1,2} and the
+        //       genotyping error probability e:
+        //          P(00|0) = 1       P(00|1) = e       P(00|2) = e^2
+        //          P(01|0) = e       P(01|1) = 1       P(01|2) = e
+        //          P(11|0) = e^2     P(11|1) = e       P(11|2) = 1
+        //
         // dsg2prob: the first index is bitmask of 8 possible dsg combinations (only 1<<0,1<<2,1<<3 are set, accessing
-        // anything else indicated an error, this is just to reuse gt_to_dsg()); the second index are the corresponding
+        // anything else indicated an error, this is just to reuse gt_to_dsg(); the second index are the corresponding
         // probabilities of 0/0, 0/1, and 1/1 genotypes
+        //
         for (i=0; i<8; i++)
             for (j=0; j<3; j++)
                 args->dsg2prob[i][j] = HUGE_VAL;
-        args->dsg2prob[1][0] = -log(1-pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[1][1] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[1][2] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[2][0] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[2][1] = -log(1-pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[2][2] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[4][0] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[4][1] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[4][2] = -log(1-pow(10,-0.1*args->use_PLs));
+        double eprob = pow(10,-0.1*args->gt_err);      // convert from phred score to probability
+        args->dsg2prob[1][0] = 0;               // P(00|0) = 1
+        args->dsg2prob[1][1] = -log(eprob);     // P(01|0) = e
+        args->dsg2prob[1][2] = -2*log(eprob);   // P(11|0) = e^2
+        args->dsg2prob[2][0] = -log(eprob);     // P(00|1) = e
+        args->dsg2prob[2][1] = 0;               // P(01|1) = 1
+        args->dsg2prob[2][2] = -log(eprob);     // P(11|1) = e
+        args->dsg2prob[4][0] = -2*log(eprob);   // P(00|2) = e^2
+        args->dsg2prob[4][1] = -log(eprob);     // P(01|2) = e
+        args->dsg2prob[4][2] = 0;               // P(11|2) = 1
 
         // lookup table to avoid exponentiation
         for (i=0; i<256; i++) args->pl2prob[i] = pow(10,-0.1*i);
     }
     else
         args->ndiff = (uint32_t*) calloc(args->npairs,sizeof(*args->ndiff));    // number of differing genotypes for each pair of samples
-    args->ncnt  = (uint32_t*) calloc(args->npairs,sizeof(*args->ncnt));         // number of comparisons performed (non-missing data)
+    args->ncnt   = (uint32_t*) calloc(args->npairs,sizeof(*args->ncnt));         // number of comparisons performed (non-missing data)
     if ( !args->ncnt ) error("Error: failed to allocate %.1f Mb\n", args->npairs*sizeof(*args->ncnt)/1e6);
     if ( args->calc_hwe_prob )
     {
         // prob of the observed sequence of matches given site AFs and HWE
         args->hwe_prob = (double*) calloc(args->npairs,sizeof(*args->hwe_prob));
         if ( !args->hwe_prob ) error("Error: failed to allocate %.1f Mb. Run with --no-HWE-prob to save some memory.\n", args->npairs*sizeof(*args->hwe_prob)/1e6);
+        args->nmatch = (uint32_t*) calloc(args->npairs,sizeof(*args->ncnt));         // number of matches, used only with calc_hwe_prob
+        if ( !args->nmatch ) error("Error: failed to allocate %.1f Mb.\n", args->npairs*sizeof(*args->ncnt)/1e6);
     }
 
     if ( args->distinctive_sites ) diff_sites_init(args);
 
-    args->fp = stdout;
-    print_header(args, args->fp);
+    args->out_fh = bgzf_open(args->output_fname, args->output_type&FT_GZ ? "wg" : "wu");
+    if ( args->out_fh == NULL )
+        error("[%s] Error: cannot write to %s: %s\n", __func__,args->output_fname ? args->output_fname : "standard output", strerror(errno));
+
+    print_header(args);
 }
 
 static void destroy_data(args_t *args)
 {
+    free(args->kstr.s);
+    if ( args->gt_filter ) filter_destroy(args->gt_filter);
+    if ( args->qry_filter ) filter_destroy(args->qry_filter);
     if ( args->gt_dsg!=args->qry_dsg ) free(args->gt_dsg);
     free(args->qry_dsg);
     if ( args->gt_prob!=args->qry_prob ) free(args->gt_prob);
     free(args->qry_prob);
     free(args->es_max_mem);
-    fclose(args->fp);
+    if ( bgzf_close(args->out_fh)!=0 )  error("Error: close failed .. %s\n", args->output_fname?args->output_fname:"stdout");
     if ( args->distinctive_sites ) diff_sites_destroy(args);
     free(args->hwe_prob);
     free(args->cwd);
@@ -429,6 +467,7 @@ static void destroy_data(args_t *args)
     free(args->pdiff);
     free(args->ndiff);
     free(args->ncnt);
+    free(args->nmatch);
     free(args->qry_smpl);
     if ( args->gt_smpl!=args->qry_smpl ) free(args->gt_smpl);
     free(args->pairs);
@@ -538,6 +577,13 @@ static void process_line(args_t *args)
     int i,j,k, nqry1, ngt1, ret;
 
     bcf1_t *gt_rec = NULL, *qry_rec = bcf_sr_get_line(args->files,0);   // the query file
+    if ( args->qry_filter )
+    {
+        int pass = filter_test(args->qry_filter, qry_rec, NULL);
+        if ( args->qry_filter_logic==FLT_EXCLUDE ) pass = pass ? 0 : 1;
+        if ( !pass ) { args->nskip_filter++; return; }
+    }
+
     int qry_use_GT = args->qry_use_GT;
     int gt_use_GT  = args->gt_use_GT;
 
@@ -547,6 +593,12 @@ static void process_line(args_t *args)
     if ( args->gt_hdr )
     {
         gt_rec = bcf_sr_get_line(args->files,1);
+        if ( args->gt_filter )
+        {
+            int pass = filter_test(args->gt_filter, gt_rec, NULL);
+            if ( args->gt_filter_logic==FLT_EXCLUDE ) pass = pass ? 0 : 1;
+            if ( !pass ) { args->nskip_filter++; return; }
+        }
         ret = set_data(args, args->gt_hdr, gt_rec, &args->gt_arr, &args->ngt_arr, &ngt1, &gt_use_GT);
         if ( ret<0 ) return;
     }
@@ -560,7 +612,7 @@ static void process_line(args_t *args)
     args->ncmp++;
     args->nused[qry_use_GT][gt_use_GT]++;
 
-    double af,hwe_dsg[8];
+    double hwe_dsg[8];
     if ( args->calc_hwe_prob )
     {
         int ac[2];
@@ -570,18 +622,28 @@ static void process_line(args_t *args)
         }
         else if ( bcf_calc_ac(args->qry_hdr, qry_rec, ac, BCF_UN_INFO|BCF_UN_FMT)!=1 ) error("todo: bcf_calc_ac() failed\n");
 
-        // hwe indexes correspond to the bitmask of eight dsg combinations to account for PL uncertainty
-        // for in the extreme case we can have uninformative PL=0,0,0. So the values are the minima of e.g.
-        //      hwe[1,2,4] ..  dsg=0,1,2
-        //      hwe[3]     ..  dsg=0 or 1
-        //      hwe[6]     ..  dsg=1 or 2
-
-        double hwe[3];
-        const double min_af = 1e-5;             // cap the AF in case we get unrealistic values
-        af = (double)ac[1]/(ac[0]+ac[1]);
-        hwe[0] = af>min_af ? -log(af*af) : -log(min_af*min_af);
-        hwe[1] = af>min_af && af<1-min_af ? -log(2*af*(1-af)) : -log(2*min_af*(1-min_af));
-        hwe[2] = af<(1-min_af) ? -log((1-af)*(1-af)) : -log(min_af*min_af);
+        // Calculate HWE probability for each possible qry+gt dosage combination. The alternate allele dosage
+        // values returned by gt_to_prob() below are 0,1,2,4 (0=missing, 1<<0, 1<<1, 1<<2). We consider only
+        // biallelic sites, therefore we work with eight genotype combinations.
+        //
+        // The array hwe_dsg is accessed with hwe_dsg[qry_dsg & gt_dsg] and is constructed to account for PL uncertainty
+        // when we encounter less informative PL, such as PL=0,0,10, where multiple dosage values are equally
+        // likely. If we allowed complete uncertainty (PL=0,0,0), we'd have up to eight possible genotype
+        // mask combinations: from e.g. 0=(gt_dsg=1<<0 & qry_dsg=1<<1) to 7=(gt_dsg=1<<0|1<<1|1<<2 & qry_dsg=1<<0|1<<1|1<<2).
+        // Note the extreme case of 1|2|4 is skipped, see pl_to_dsg().
+        //
+        // When the dosage is uncertain, we take the minimum of their corresponding HWE value, for example
+        //      hwe[0] = 0
+        //      hwe[1] = (1-AF)**2
+        //      hwe[2] = 2*AF*(1-AF)
+        //      hwe[4] = AF**2
+        //      hwe[3] = min{hwe[1],hwe[2]}
+
+        double hwe[3];  // while hwe_dsg iterates over dsg bitmasks (0..7), hwe iterates over dsg (0,1,2)
+        double af = ac[0]+ac[1] ? (double)ac[1]/(ac[0]+ac[1]) : 1e-6;
+        hwe[0] = -log((1-af)*(1-af));
+        hwe[1] = -log(2*af*(1-af));
+        hwe[2] = -log(af*af);
         hwe_dsg[0] = 0;
         for (i=1; i<8; i++)
         {
@@ -596,7 +658,7 @@ static void process_line(args_t *args)
     // The sample pairs were given explicitly via -p/-P options
     if ( args->pairs )
     {
-        if ( !args->use_PLs )
+        if ( !args->gt_err )
         {
             int ndiff = 0;
             if ( args->kbs_diff ) diff_sites_reset(args);
@@ -621,13 +683,17 @@ static void process_line(args_t *args)
                     args->ndiff[i]++;
                     if ( args->kbs_diff ) { ndiff++; kbs_insert(args->kbs_diff, i); }
                 }
-                else if ( args->calc_hwe_prob ) args->hwe_prob[i] += hwe_dsg[match];
+                else if ( args->calc_hwe_prob )
+                {
+                    args->hwe_prob[i] += hwe_dsg[match];
+                    args->nmatch[i]++;
+                }
                 args->ncnt[i]++;
             }
 
             if ( ndiff ) diff_sites_push(args, ndiff, qry_rec->rid, qry_rec->pos);
         }
-        else    // use_PLs set
+        else    // gt_err set
         {
             for (i=0; i<args->npairs; i++)
             {
@@ -655,6 +721,7 @@ static void process_line(args_t *args)
                 {
                     int match = qry_dsg & gt_dsg;
                     args->hwe_prob[i] += hwe_dsg[match];
+                    if ( match ) args->nmatch[i]++;
                 }
                 args->ncnt[i]++;
             }
@@ -663,7 +730,7 @@ static void process_line(args_t *args)
     }
 
     int idx=0;
-    if ( !args->use_PLs )
+    if ( !args->gt_err )
     {
         for (i=0; i<args->nqry_smpl; i++)
         {
@@ -690,13 +757,17 @@ static void process_line(args_t *args)
                 if ( !args->gt_dsg[j] ) { idx++; continue; }        // missing value
                 int match = args->qry_dsg[i] & args->gt_dsg[j];
                 if ( !match ) args->ndiff[idx]++;
-                else if ( args->calc_hwe_prob ) args->hwe_prob[idx] += hwe_dsg[match];
+                else if ( args->calc_hwe_prob )
+                {
+                    args->hwe_prob[idx] += hwe_dsg[match];
+                    args->nmatch[idx]++;
+                }
                 args->ncnt[idx]++;
                 idx++;
             }
         }
     }
-    else    // use_PLs set
+    else    // gt_err set
     {
         for (i=0; i<args->nqry_smpl; i++)
         {
@@ -731,6 +802,7 @@ static void process_line(args_t *args)
                 {
                     int match = args->qry_dsg[i] & args->gt_dsg[j];
                     args->hwe_prob[idx] += hwe_dsg[match];
+                    if ( match ) args->nmatch[idx]++;
                 }
                 args->ncnt[idx]++;
                 idx++;
@@ -758,12 +830,13 @@ static void report_distinctive_sites(args_t *args)
 {
     extsort_sort(args->es);
 
-    fprintf(args->fp,"# DS, distinctive sites:\n");
-    fprintf(args->fp,"#     - chromosome\n");
-    fprintf(args->fp,"#     - position\n");
-    fprintf(args->fp,"#     - cumulative number of pairs distinguished by this block\n");
-    fprintf(args->fp,"#     - block id\n");
-    fprintf(args->fp,"#DS\t[2]Chromosome\t[3]Position\t[4]Cumulative number of distinct pairs\t[5]Block id\n");
+    args->kstr.l = 0;
+    ksprintf(&args->kstr,"# DS, distinctive sites:\n");
+    ksprintf(&args->kstr,"#     - chromosome\n");
+    ksprintf(&args->kstr,"#     - position\n");
+    ksprintf(&args->kstr,"#     - cumulative number of pairs distinguished by this block\n");
+    ksprintf(&args->kstr,"#     - block id\n");
+    ksprintf(&args->kstr,"#DS\t[2]Chromosome\t[3]Position\t[4]Cumulative number of distinct pairs\t[5]Block id\n");
 
     kbitset_t *kbs_blk = kbs_init(args->npairs);
     kbitset_iter_t itr;
@@ -783,7 +856,9 @@ static void report_distinctive_sites(args_t *args)
         if ( ndiff_dbg!=ndiff ) error("Corrupted data, fixme: %d vs %d\n",ndiff_dbg,ndiff);
         if ( !ndiff_new ) continue;     // no new pair distinguished by this site
         ndiff_tot += ndiff_new;
-        fprintf(args->fp,"DS\t%s\t%d\t%d\t%d\n",bcf_hdr_id2name(args->qry_hdr,rid),pos+1,ndiff_tot,iblock);
+        args->kstr.l = 0;
+        ksprintf(&args->kstr,"DS\t%s\t%d\t%d\t%d\n",bcf_hdr_id2name(args->qry_hdr,rid),pos+1,ndiff_tot,iblock);
+        if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
         if ( ndiff_tot < ndiff_min ) continue;   // fewer than the requested number of pairs can be distinguished at this point
         iblock++;
         ndiff_tot = 0;
@@ -793,24 +868,35 @@ static void report_distinctive_sites(args_t *args)
 }
 static void report(args_t *args)
 {
-    fprintf(args->fp,"INFO\tsites-compared\t%u\n",args->ncmp);
-    fprintf(args->fp,"INFO\tsites-skipped-no-match\t%u\n",args->nskip_no_match);
-    fprintf(args->fp,"INFO\tsites-skipped-multiallelic\t%u\n",args->nskip_not_ba);
-    fprintf(args->fp,"INFO\tsites-skipped-monoallelic\t%u\n",args->nskip_mono);
-    fprintf(args->fp,"INFO\tsites-skipped-no-data\t%u\n",args->nskip_no_data);
-    fprintf(args->fp,"INFO\tsites-skipped-GT-not-diploid\t%u\n",args->nskip_dip_GT);
-    fprintf(args->fp,"INFO\tsites-skipped-PL-not-diploid\t%u\n",args->nskip_dip_PL);
-    fprintf(args->fp,"INFO\tsites-used-PL-vs-PL\t%u\n",args->nused[0][0]);
-    fprintf(args->fp,"INFO\tsites-used-PL-vs-GT\t%u\n",args->nused[0][1]);
-    fprintf(args->fp,"INFO\tsites-used-GT-vs-PL\t%u\n",args->nused[1][0]);
-    fprintf(args->fp,"INFO\tsites-used-GT-vs-GT\t%u\n",args->nused[1][1]);
-    fprintf(args->fp,"# DC, discordance:\n");
-    fprintf(args->fp,"#     - query sample\n");
-    fprintf(args->fp,"#     - genotyped sample\n");
-    fprintf(args->fp,"#     - discordance (either an abstract score or number of mismatches, see -e/-u in the man page for details; smaller is better)\n");
-    fprintf(args->fp,"#     - negative log of HWE probability at matching sites (rare genotypes matches are more informative, bigger is better)\n");
-    fprintf(args->fp,"#     - number of sites compared (bigger is better)\n");
-    fprintf(args->fp,"#DC\t[2]Query Sample\t[3]Genotyped Sample\t[4]Discordance\t[5]-log P(HWE)\t[6]Number of sites compared\n");
+    args->kstr.l = 0;
+    ksprintf(&args->kstr,"INFO\tsites-compared\t%u\n",args->ncmp);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-no-match\t%u\n",args->nskip_no_match);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-multiallelic\t%u\n",args->nskip_not_ba);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-monoallelic\t%u\n",args->nskip_mono);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-no-data\t%u\n",args->nskip_no_data);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-GT-not-diploid\t%u\n",args->nskip_dip_GT);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-PL-not-diploid\t%u\n",args->nskip_dip_PL);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-filtering-expression\t%u\n",args->nskip_filter);
+    ksprintf(&args->kstr,"INFO\tsites-used-PL-vs-PL\t%u\n",args->nused[0][0]);
+    ksprintf(&args->kstr,"INFO\tsites-used-PL-vs-GT\t%u\n",args->nused[0][1]);
+    ksprintf(&args->kstr,"INFO\tsites-used-GT-vs-PL\t%u\n",args->nused[1][0]);
+    ksprintf(&args->kstr,"INFO\tsites-used-GT-vs-GT\t%u\n",args->nused[1][1]);
+    ksprintf(&args->kstr,"# DCv2, discordance version 2:\n");
+    ksprintf(&args->kstr,"#     - Query sample\n");
+    ksprintf(&args->kstr,"#     - Genotyped sample\n");
+    ksprintf(&args->kstr,"#     - Discordance, given either as an abstract score or number of mismatches, see the options -E/-u\n"
+                         "#       in man page for details. Note that samples with high missingness have fewer sites compared,\n"
+                         "#       which results in lower overall discordance. Therefore it is advisable to use the average score\n"
+                         "#       per site rather than the absolute value, i.e. divide the value by the number of sites compared\n"
+                         "#       (smaller value = better match)\n");
+    ksprintf(&args->kstr,"#     - Average negative log of HWE probability at matching sites, attempts to quantify the following\n"
+                         "#       intuition: rare genotype matches are more informative than common genotype matches, hence two\n"
+                         "#       samples with similar discordance can be further stratified by the HWE score (bigger value = better\n"
+                         "#       match, the observed concordance was less likely to occur by chance)\n");
+    ksprintf(&args->kstr,"#     - Number of sites compared for this pair of samples (bigger = more informative)\n");
+    ksprintf(&args->kstr,"#     - Number of matching genotypes\n");
+    ksprintf(&args->kstr,"#DCv2\t[2]Query Sample\t[3]Genotyped Sample\t[4]Discordance\t[5]Average -log P(HWE)\t[6]Number of sites compared\t[7]Number of matching genotypes\n");
+    if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
 
     int trim = args->ntop;
     if ( !args->pairs )
@@ -824,26 +910,30 @@ static void report(args_t *args)
         int i;
         for (i=0; i<args->npairs; i++)
         {
+            args->kstr.l = 0;
             int iqry = args->pairs[i].iqry;
             int igt  = args->pairs[i].igt;
             if ( args->ndiff )
             {
-                fprintf(args->fp,"DC\t%s\t%s\t%u\t%e\t%u\n",
+                ksprintf(&args->kstr,"DCv2\t%s\t%s\t%u\t%e\t%u\t%u\n",
                         args->qry_hdr->samples[iqry],
                         args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                         args->ndiff[i],
-                        args->calc_hwe_prob ? args->hwe_prob[i] : 0,
-                        args->ncnt[i]);
+                        (args->calc_hwe_prob && args->nmatch[i]) ? args->hwe_prob[i]/args->nmatch[i] : 0,
+                        args->ncnt[i],
+                        args->nmatch[i]);
             }
             else
             {
-                fprintf(args->fp,"DC\t%s\t%s\t%e\t%e\t%u\n",
+                ksprintf(&args->kstr,"DCv2\t%s\t%s\t%e\t%e\t%u\t%u\n",
                         args->qry_hdr->samples[iqry],
                         args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                         args->pdiff[i],
-                        args->calc_hwe_prob ? args->hwe_prob[i] : 0,
-                        args->ncnt[i]);
+                        (args->calc_hwe_prob && args->nmatch[i]) ? args->hwe_prob[i]/args->nmatch[i] : 0,
+                        args->ncnt[i],
+                        args->nmatch[i]);
             }
+            if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
         }
     }
     else if ( !trim )
@@ -855,25 +945,29 @@ static void report(args_t *args)
             int ngt  = args->cross_check ? i : args->ngt_smpl;
             for (j=0; j<ngt; j++)
             {
+                args->kstr.l = 0;
                 int igt = args->gt_smpl ? args->gt_smpl[j] : j;
                 if ( args->ndiff )
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%u\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%u\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                             args->ndiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
                 else
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%e\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%e\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                             args->pdiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
+                if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
                 idx++;
             }
         }
@@ -888,7 +982,7 @@ static void report(args_t *args)
             for (j=0; j<args->ngt_smpl; j++)
             {
                 if ( args->sort_by_hwe )
-                    arr[j].val = -args->hwe_prob[idx];
+                    arr[j].val = args->nmatch[idx] ? -args->hwe_prob[idx]/args->nmatch[idx] : 0;  // -args->hwe_prob[idx];
                 else if ( args->ndiff )
                     arr[j].val = args->ncnt[idx] ? (double)args->ndiff[idx]/args->ncnt[idx] : 0;
                 else
@@ -901,26 +995,30 @@ static void report(args_t *args)
             int iqry = args->qry_smpl ? args->qry_smpl[i] : i;
             for (j=0; j<args->ntop; j++)
             {
+                args->kstr.l = 0;
                 int idx = arr[j].idx;
                 int igt = args->gt_smpl ? args->gt_smpl[arr[j].ism] : arr[j].ism;
                 if ( args->ndiff )
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%u\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%u\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                             args->ndiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
                 else
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%e\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%e\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                             args->pdiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
+                if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
             }
         }
         free(arr);
@@ -936,7 +1034,7 @@ static void report(args_t *args)
             for (j=0; j<i; j++)
             {
                 if ( args->sort_by_hwe )
-                    arr[k].val = -args->hwe_prob[idx];
+                    arr[k].val = args->nmatch[idx] ? -args->hwe_prob[idx]/args->nmatch[idx] : 0;
                 else if ( args->ndiff )
                     arr[k].val = args->ncnt[idx] ? (double)args->ndiff[idx]/args->ncnt[idx] : 0;
                 else
@@ -950,7 +1048,7 @@ static void report(args_t *args)
             {
                 idx = j*(j+1)/2 + i;
                 if ( args->sort_by_hwe )
-                    arr[k].val = -args->hwe_prob[idx];
+                    arr[k].val = args->nmatch[idx] ? -args->hwe_prob[idx]/args->nmatch[idx] : 0;
                 else if ( args->ndiff )
                     arr[k].val = args->ncnt[idx] ? (double)args->ndiff[idx]/args->ncnt[idx] : 0;
                 else
@@ -963,27 +1061,31 @@ static void report(args_t *args)
             int iqry = args->qry_smpl ? args->qry_smpl[i] : i;
             for (j=0; j<args->ntop; j++)
             {
+                args->kstr.l = 0;
                 if ( i <= arr[j].ism ) continue;
                 int idx = arr[j].idx;
                 int igt = args->qry_smpl ? args->qry_smpl[arr[j].ism] : arr[j].ism;
                 if ( args->ndiff )
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%u\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%u\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->qry_hdr->samples[igt],
                             args->ndiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
                 else
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%e\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%e\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->qry_hdr->samples[igt],
                             args->pdiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
+                if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
             }
         }
         free(arr);
@@ -1053,12 +1155,16 @@ static void usage(void)
     fprintf(stderr, "                                           and TMP is a prefix of temporary files used by external sorting [/tmp/bcftools.XXXXXX]\n");
 #endif
     fprintf(stderr, "        --dry-run                      Stop after first record to estimate required time\n");
-    fprintf(stderr, "    -e, --error-probability INT        Phred-scaled probability of genotyping error, 0 for faster but less accurate results [40]\n");
+    fprintf(stderr, "    -E, --error-probability INT        Phred-scaled probability of genotyping error, 0 for faster but less accurate results [40]\n");
+    fprintf(stderr, "    -e, --exclude [qry|gt]:EXPR        Exclude sites for which the expression is true\n");
     fprintf(stderr, "    -g, --genotypes FILE               Genotypes to compare against\n");
     fprintf(stderr, "    -H, --homs-only                    Homozygous genotypes only, useful with low coverage data (requires -g)\n");
+    fprintf(stderr, "    -i, --include [qry|gt]:EXPR        Include sites for which the expression is true\n");
     fprintf(stderr, "        --n-matches INT                Print only top INT matches for each sample (sorted by average score), 0 for unlimited.\n");
     fprintf(stderr, "                                           Use negative value to sort by HWE probability rather than by discordance [0]\n");
     fprintf(stderr, "        --no-HWE-prob                  Disable calculation of HWE probability\n");
+    fprintf(stderr, "    -o, --output FILE                  Write output to a file [standard output]\n");
+    fprintf(stderr, "    -O, --output-type t|z              t: plain tab-delimited text output, z: compressed [t]\n");
     fprintf(stderr, "    -p, --pairs LIST                   Comma-separated sample pairs to compare (qry,gt[,qry,gt..] with -g or qry,qry[,qry,qry..] w/o)\n");
     fprintf(stderr, "    -P, --pairs-file FILE              File with tab-delimited sample pairs to compare (qry,gt with -g or qry,qry w/o)\n");
     fprintf(stderr, "    -r, --regions REGION               Restrict to comma-separated list of regions\n");
@@ -1071,10 +1177,10 @@ static void usage(void)
     fprintf(stderr, "        --targets-overlap 0|1|2        Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n");
     fprintf(stderr, "    -u, --use TAG1[,TAG2]              Which tag to use in the query file (TAG1) and the -g file (TAG2) [PL,GT]\n");
     fprintf(stderr, "Examples:\n");
-    fprintf(stderr, "   # Check discordance of all samples from B against all sample in A\n");
+    fprintf(stderr, "   # Check discordance of all samples from B against all samples in A\n");
     fprintf(stderr, "   bcftools gtcheck -g A.bcf B.bcf\n");
     fprintf(stderr, "\n");
-    fprintf(stderr, "   # Limit comparisons to the fiven list of samples\n");
+    fprintf(stderr, "   # Limit comparisons to the given list of samples\n");
     fprintf(stderr, "   bcftools gtcheck -s gt:a1,a2,a3 -s qry:b1,b2 -g A.bcf B.bcf\n");
     fprintf(stderr, "\n");
     fprintf(stderr, "   # Compare only two pairs a1,b1 and a1,b2\n");
@@ -1091,9 +1197,10 @@ int main_vcfgtcheck(int argc, char *argv[])
     args->qry_use_GT = -1;
     args->gt_use_GT  = -1;
     args->calc_hwe_prob = 1;
-    args->use_PLs = 40;
+    args->gt_err = 40;
     args->regions_overlap = 1;
     args->targets_overlap = 0;
+    args->output_fname = "-";
 
     // external sort for --distinctive-sites
 #ifdef _WIN32
@@ -1112,7 +1219,11 @@ int main_vcfgtcheck(int argc, char *argv[])
 
     static struct option loptions[] =
     {
-        {"error-probability",1,0,'e'},
+        {"error-probability",1,0,'E'},  // note this used to be 'e', but can easily auto-detect to assure backward compatibility
+        {"exclude",required_argument,0,'e'},
+        {"include",required_argument,0,'i'},
+        {"output",required_argument,0,'o'},
+        {"output-type",required_argument,NULL,'O'},
         {"use",1,0,'u'},
         {"cluster",1,0,'c'},
         {"GTs-only",1,0,'G'},
@@ -1139,10 +1250,79 @@ int main_vcfgtcheck(int argc, char *argv[])
         {0,0,0,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "hg:p:s:S:p:P:Hr:R:at:T:G:c:u:e:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hg:p:s:S:p:P:Hr:R:at:T:G:c:u:e:E:i:o:O:",loptions,NULL)) >= 0) {
         switch (c) {
+            case 'o': args->output_fname = optarg; break;
+            case 'O':
+                switch (optarg[0]) {
+                    case 't': args->output_type = FT_TAB_TEXT; break;
+                    case 'z': args->output_type = FT_VCF_GZ; break;
+                    default:
+                    {
+                        args->clevel = strtol(optarg,&tmp,10);
+                        if ( *tmp || args->clevel<0 || args->clevel>9 ) error("The output type \"%s\" not recognised\n", optarg);
+                    }
+                }
+                if ( optarg[1] )
+                {
+                    args->clevel = strtol(optarg+1,&tmp,10);
+                    if ( *tmp || args->clevel<0 || args->clevel>9 ) error("Could not parse argument: --output-type %s\n", optarg+1);
+                }
+                break;
             case 'e':
-                args->use_PLs = strtol(optarg,&tmp,10);
+                if ( !strncasecmp("gt:",optarg,3) )
+                {
+                    if ( args->gt_filter_str ) error("Error: only one -i or -e expression can be given for gt:, and they cannot be combined\n");
+                    args->gt_filter_str = optarg;
+                    args->gt_filter_logic |= FLT_EXCLUDE;
+                }
+                else if ( !strncasecmp("qry:",optarg,4) )
+                {
+                    if ( args->qry_filter_str ) error("Error: only one -i or -e expression can be given for qry:, and they cannot be combined\n");
+                    args->qry_filter_str = optarg;
+                    args->qry_filter_logic |= FLT_EXCLUDE;
+                }
+                else
+                {
+                    // this could be the old -e, --error-probability option
+                    args->gt_err = strtol(optarg,&tmp,10);
+                    if ( !tmp || *tmp )
+                    {
+                        // it is not
+                        args->gt_filter_str  = optarg;
+                        args->qry_filter_str = optarg;
+                        args->gt_filter_logic  |= FLT_EXCLUDE;
+                        args->qry_filter_logic |= FLT_EXCLUDE;
+                    }
+                    else
+                    {
+                        fprintf(stderr,"[warning] auto-detected the old format --error-probability option, please switch from -e to -E.\n");
+                    }
+                }
+                break;
+            case 'i':
+                if ( !strncasecmp("gt:",optarg,3) )
+                {
+                    if ( args->gt_filter_str ) error("Error: only one -i or -e expression can be given for gt:, and they cannot be combined\n");
+                    args->gt_filter_str = optarg;
+                    args->gt_filter_logic |= FLT_INCLUDE;
+                }
+                else if ( !strncasecmp("qry:",optarg,4) )
+                {
+                    if ( args->qry_filter_str ) error("Error: only one -i or -e expression can be given for qry:, and they cannot be combined\n");
+                    args->qry_filter_str = optarg;
+                    args->qry_filter_logic |= FLT_INCLUDE;
+                }
+                else
+                {
+                    args->gt_filter_str  = optarg;
+                    args->qry_filter_str = optarg;
+                    args->gt_filter_logic  |= FLT_INCLUDE;
+                    args->qry_filter_logic |= FLT_INCLUDE;
+                }
+                break;
+            case 'E':
+                args->gt_err = strtol(optarg,&tmp,10);
                 if ( !tmp || *tmp ) error("Could not parse: --error-probability %s\n", optarg);
                 break;
             case 'u':
@@ -1187,7 +1367,7 @@ int main_vcfgtcheck(int argc, char *argv[])
                     while ( *tmp && *tmp!=',' ) tmp++;
                     if ( *tmp ) { *tmp = 0; args->es_tmp_prefix = tmp+1; }
                 }
-                args->use_PLs = 0;
+                args->gt_err = 0;
                 break;
             case 'c':
                 error("The -c option is to be implemented, please open an issue on github\n");
@@ -1247,7 +1427,7 @@ int main_vcfgtcheck(int argc, char *argv[])
     }
     if ( args->distinctive_sites && !args->pair_samples ) error("The experimental option --distinctive-sites requires -p/-P\n");
     if ( args->hom_only && !args->gt_fname ) error("The option --homs-only requires --genotypes\n");
-    if ( args->distinctive_sites && args->use_PLs ) error("The option --distinctive-sites cannot be combined with --error-probability\n");
+    if ( args->distinctive_sites && args->gt_err ) error("The option --distinctive-sites cannot be combined with --error-probability\n");
 
     init_data(args);
 
@@ -1267,7 +1447,9 @@ int main_vcfgtcheck(int argc, char *argv[])
             gettimeofday(&t1, NULL);
             double delta = (t1.tv_sec - t0.tv_sec) * 1e6 + (t1.tv_usec - t0.tv_usec);
             fprintf(stderr,"INFO:\tTime required to process one record .. %f seconds\n",delta/1e6);
-            fprintf(args->fp,"INFO\tTime required to process one record .. %f seconds\n",delta/1e6);
+            args->kstr.l = 0;
+            ksprintf(&args->kstr,"INFO\tTime required to process one record .. %f seconds\n",delta/1e6);
+            if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
             if ( args->dry_run ) break;
         }
     }
index 54568b0544802098070c6c11532560f9fa537d4d..de7c616245de307333ef7561fa40009b89217acb 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  vcfgtcheck.c -- Check sample identity.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -41,11 +41,17 @@ THE SOFTWARE.  */
 #include <htslib/vcfutils.h>
 #include <htslib/kbitset.h>
 #include <htslib/hts_os.h>
+#include <htslib/bgzf.h>
 #include <inttypes.h>
 #include <sys/time.h>
 #include "bcftools.h"
 #include "extsort.h"
 //#include "hclust.h"
+#include "filter.h"
+
+// Logic of the filters: include or exclude sites which match the filters?
+#define FLT_INCLUDE 1
+#define FLT_EXCLUDE 2
 
 typedef struct
 {
@@ -58,20 +64,22 @@ typedef struct
     bcf_srs_t *files;           // first reader is the query VCF - single sample normally or multi-sample for cross-check
     bcf_hdr_t *gt_hdr, *qry_hdr; // VCF with genotypes to compare against and the query VCF
     char *cwd, **argv, *gt_samples, *qry_samples, *regions, *targets, *qry_fname, *gt_fname, *pair_samples;
-    int argc, gt_samples_is_file, qry_samples_is_file, regions_is_file, targets_is_file, pair_samples_is_file;
-    int regions_overlap, targets_overlap;
+    char *output_fname;
+    int argc, gt_samples_is_file, qry_samples_is_file, regions_is_file, targets_is_file, pair_samples_is_file, output_type;;
+    int regions_overlap, targets_overlap, clevel;
     int qry_use_GT,gt_use_GT, nqry_smpl,ngt_smpl, *qry_smpl,*gt_smpl;
     int nused[2][2];
     double *pdiff, *qry_prob, *gt_prob;
-    uint32_t *ndiff,*ncnt,ncmp, npairs;
+    uint32_t *ndiff,*ncnt,*nmatch,ncmp, npairs;
     int32_t *qry_arr,*gt_arr, nqry_arr,ngt_arr;
     uint8_t *qry_dsg, *gt_dsg;
     pair_t *pairs;
     double *hwe_prob, dsg2prob[8][3], pl2prob[256];
     double min_inter_err, max_intra_err;
-    int all_sites, hom_only, ntop, cross_check, calc_hwe_prob, sort_by_hwe, dry_run, use_PLs;
-    FILE *fp;
-    unsigned int nskip_no_match, nskip_not_ba, nskip_mono, nskip_no_data, nskip_dip_GT, nskip_dip_PL;
+    int all_sites, hom_only, ntop, cross_check, calc_hwe_prob, sort_by_hwe, dry_run, gt_err;
+    BGZF *out_fh;
+    unsigned int nskip_no_match, nskip_not_ba, nskip_mono, nskip_no_data, nskip_dip_GT, nskip_dip_PL, nskip_filter;
+    kstring_t kstr;
 
     // for --distinctive-sites
     double distinctive_sites;
@@ -79,6 +87,11 @@ typedef struct
     size_t diff_sites_size;
     extsort_t *es;
     char *es_tmp_prefix, *es_max_mem;
+
+    // include or exclude sites which match the filters
+    filter_t *qry_filter, *gt_filter;
+    char *qry_filter_str, *gt_filter_str;
+    int qry_filter_logic, gt_filter_logic;       // FLT_INCLUDE or FLT_EXCLUDE
 }
 args_t;
 
@@ -96,15 +109,17 @@ static void set_cwd(args_t *args)
     }
     assert(buf);
 }
-static void print_header(args_t *args, FILE *fp)
+static void print_header(args_t *args)
 {
-    fprintf(fp, "# This file was produced by bcftools (%s+htslib-%s), the command line was:\n", bcftools_version(), hts_version());
-    fprintf(fp, "# \t bcftools %s ", args->argv[0]);
+    args->kstr.l = 0;
+    ksprintf(&args->kstr, "# This file was produced by bcftools (%s+htslib-%s), the command line was:\n", bcftools_version(), hts_version());
+    ksprintf(&args->kstr, "# \t bcftools %s ", args->argv[0]);
     int i;
     for (i=1; i<args->argc; i++)
-        fprintf(fp, " %s",args->argv[i]);
-    fprintf(fp, "\n# and the working directory was:\n");
-    fprintf(fp, "# \t %s\n#\n", args->cwd);
+        ksprintf(&args->kstr, " %s",args->argv[i]);
+    ksprintf(&args->kstr, "\n# and the working directory was:\n");
+    ksprintf(&args->kstr, "# \t %s\n#\n", args->cwd);
+    if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
 }
 
 static int cmp_int(const void *_a, const void *_b)
@@ -128,7 +143,7 @@ static int cmp_pair(const void *_a, const void *_b)
 
 typedef struct
 {
-    uint32_t ndiff,rid,pos,rand; // rand is to shuffle sites with the same ndiff from across all chromosoms
+    uint32_t ndiff,rid,pos,rand; // rand is to shuffle sites with the same ndiff from across all chromosomes
     unsigned long kbs_dat[1];
 }
 diff_sites_t;
@@ -264,6 +279,9 @@ static void init_data(args_t *args)
         if ( !bcf_hdr_nsamples(args->gt_hdr) ) error("No samples in %s?\n", args->gt_fname);
     }
 
+    if ( args->gt_hdr && args->gt_filter_str ) args->gt_filter = filter_init(args->gt_hdr, args->gt_filter_str);
+    if ( args->qry_hdr && args->qry_filter_str ) args->qry_filter = filter_init(args->qry_hdr, args->qry_filter_str);
+
     // Determine whether GT or PL will be used
     if ( args->qry_use_GT==-1 ) // not set by -u, qry uses PL by default
     {
@@ -373,56 +391,76 @@ static void init_data(args_t *args)
         args->qry_dsg = (uint8_t*) malloc(args->nqry_smpl);
         args->gt_dsg  = args->cross_check ? args->qry_dsg : (uint8_t*) malloc(args->ngt_smpl);
     }
-    if ( args->use_PLs )
+    if ( args->gt_err )
     {
         args->pdiff = (double*) calloc(args->npairs,sizeof(*args->pdiff));      // log probability of pair samples being the same
         args->qry_prob = (double*) malloc(3*args->nqry_smpl*sizeof(*args->qry_prob));
         args->gt_prob  = args->cross_check ? args->qry_prob : (double*) malloc(3*args->ngt_smpl*sizeof(*args->gt_prob));
 
+        // Convert genotypes to genotype likelihoods given by -E, the probability of reading one allele incorrectly. In this
+        // simple model we have:
+        //     - probability of reading an allele incorrectly, eg. 0 as 1 or 1 as 0
+        //         P(0|1) = P(1|0) = e
+        //     - probability of genotype G={00,01,11} being correct given observed dosage {0,1,2} and the
+        //       genotyping error probability e:
+        //          P(00|0) = 1       P(00|1) = e       P(00|2) = e^2
+        //          P(01|0) = e       P(01|1) = 1       P(01|2) = e
+        //          P(11|0) = e^2     P(11|1) = e       P(11|2) = 1
+        //
         // dsg2prob: the first index is bitmask of 8 possible dsg combinations (only 1<<0,1<<2,1<<3 are set, accessing
-        // anything else indicated an error, this is just to reuse gt_to_dsg()); the second index are the corresponding
+        // anything else indicated an error, this is just to reuse gt_to_dsg(); the second index are the corresponding
         // probabilities of 0/0, 0/1, and 1/1 genotypes
+        //
         for (i=0; i<8; i++)
             for (j=0; j<3; j++)
                 args->dsg2prob[i][j] = HUGE_VAL;
-        args->dsg2prob[1][0] = -log(1-pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[1][1] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[1][2] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[2][0] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[2][1] = -log(1-pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[2][2] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[4][0] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[4][1] = -log(0.5*pow(10,-0.1*args->use_PLs));
-        args->dsg2prob[4][2] = -log(1-pow(10,-0.1*args->use_PLs));
+        double eprob = pow(10,-0.1*args->gt_err);      // convert from phred score to probability
+        args->dsg2prob[1][0] = 0;               // P(00|0) = 1
+        args->dsg2prob[1][1] = -log(eprob);     // P(01|0) = e
+        args->dsg2prob[1][2] = -2*log(eprob);   // P(11|0) = e^2
+        args->dsg2prob[2][0] = -log(eprob);     // P(00|1) = e
+        args->dsg2prob[2][1] = 0;               // P(01|1) = 1
+        args->dsg2prob[2][2] = -log(eprob);     // P(11|1) = e
+        args->dsg2prob[4][0] = -2*log(eprob);   // P(00|2) = e^2
+        args->dsg2prob[4][1] = -log(eprob);     // P(01|2) = e
+        args->dsg2prob[4][2] = 0;               // P(11|2) = 1
 
         // lookup table to avoid exponentiation
         for (i=0; i<256; i++) args->pl2prob[i] = pow(10,-0.1*i);
     }
     else
         args->ndiff = (uint32_t*) calloc(args->npairs,sizeof(*args->ndiff));    // number of differing genotypes for each pair of samples
-    args->ncnt  = (uint32_t*) calloc(args->npairs,sizeof(*args->ncnt));         // number of comparisons performed (non-missing data)
+    args->ncnt   = (uint32_t*) calloc(args->npairs,sizeof(*args->ncnt));         // number of comparisons performed (non-missing data)
     if ( !args->ncnt ) error("Error: failed to allocate %.1f Mb\n", args->npairs*sizeof(*args->ncnt)/1e6);
     if ( args->calc_hwe_prob )
     {
         // prob of the observed sequence of matches given site AFs and HWE
         args->hwe_prob = (double*) calloc(args->npairs,sizeof(*args->hwe_prob));
         if ( !args->hwe_prob ) error("Error: failed to allocate %.1f Mb. Run with --no-HWE-prob to save some memory.\n", args->npairs*sizeof(*args->hwe_prob)/1e6);
+        args->nmatch = (uint32_t*) calloc(args->npairs,sizeof(*args->ncnt));         // number of matches, used only with calc_hwe_prob
+        if ( !args->nmatch ) error("Error: failed to allocate %.1f Mb.\n", args->npairs*sizeof(*args->ncnt)/1e6);
     }
 
     if ( args->distinctive_sites ) diff_sites_init(args);
 
-    args->fp = bcftools_stdout;
-    print_header(args, args->fp);
+    args->out_fh = bgzf_open(args->output_fname, args->output_type&FT_GZ ? "wg" : "wu");
+    if ( args->out_fh == NULL )
+        error("[%s] Error: cannot write to %s: %s\n", __func__,args->output_fname ? args->output_fname : "standard output", strerror(errno));
+
+    print_header(args);
 }
 
 static void destroy_data(args_t *args)
 {
+    free(args->kstr.s);
+    if ( args->gt_filter ) filter_destroy(args->gt_filter);
+    if ( args->qry_filter ) filter_destroy(args->qry_filter);
     if ( args->gt_dsg!=args->qry_dsg ) free(args->gt_dsg);
     free(args->qry_dsg);
     if ( args->gt_prob!=args->qry_prob ) free(args->gt_prob);
     free(args->qry_prob);
     free(args->es_max_mem);
-    fclose(args->fp);
+    if ( bgzf_close(args->out_fh)!=0 )  error("Error: close failed .. %s\n", args->output_fname?args->output_fname:"bcftools_stdout");
     if ( args->distinctive_sites ) diff_sites_destroy(args);
     free(args->hwe_prob);
     free(args->cwd);
@@ -431,6 +469,7 @@ static void destroy_data(args_t *args)
     free(args->pdiff);
     free(args->ndiff);
     free(args->ncnt);
+    free(args->nmatch);
     free(args->qry_smpl);
     if ( args->gt_smpl!=args->qry_smpl ) free(args->gt_smpl);
     free(args->pairs);
@@ -540,6 +579,13 @@ static void process_line(args_t *args)
     int i,j,k, nqry1, ngt1, ret;
 
     bcf1_t *gt_rec = NULL, *qry_rec = bcf_sr_get_line(args->files,0);   // the query file
+    if ( args->qry_filter )
+    {
+        int pass = filter_test(args->qry_filter, qry_rec, NULL);
+        if ( args->qry_filter_logic==FLT_EXCLUDE ) pass = pass ? 0 : 1;
+        if ( !pass ) { args->nskip_filter++; return; }
+    }
+
     int qry_use_GT = args->qry_use_GT;
     int gt_use_GT  = args->gt_use_GT;
 
@@ -549,6 +595,12 @@ static void process_line(args_t *args)
     if ( args->gt_hdr )
     {
         gt_rec = bcf_sr_get_line(args->files,1);
+        if ( args->gt_filter )
+        {
+            int pass = filter_test(args->gt_filter, gt_rec, NULL);
+            if ( args->gt_filter_logic==FLT_EXCLUDE ) pass = pass ? 0 : 1;
+            if ( !pass ) { args->nskip_filter++; return; }
+        }
         ret = set_data(args, args->gt_hdr, gt_rec, &args->gt_arr, &args->ngt_arr, &ngt1, &gt_use_GT);
         if ( ret<0 ) return;
     }
@@ -562,7 +614,7 @@ static void process_line(args_t *args)
     args->ncmp++;
     args->nused[qry_use_GT][gt_use_GT]++;
 
-    double af,hwe_dsg[8];
+    double hwe_dsg[8];
     if ( args->calc_hwe_prob )
     {
         int ac[2];
@@ -572,18 +624,28 @@ static void process_line(args_t *args)
         }
         else if ( bcf_calc_ac(args->qry_hdr, qry_rec, ac, BCF_UN_INFO|BCF_UN_FMT)!=1 ) error("todo: bcf_calc_ac() failed\n");
 
-        // hwe indexes correspond to the bitmask of eight dsg combinations to account for PL uncertainty
-        // for in the extreme case we can have uninformative PL=0,0,0. So the values are the minima of e.g.
-        //      hwe[1,2,4] ..  dsg=0,1,2
-        //      hwe[3]     ..  dsg=0 or 1
-        //      hwe[6]     ..  dsg=1 or 2
-
-        double hwe[3];
-        const double min_af = 1e-5;             // cap the AF in case we get unrealistic values
-        af = (double)ac[1]/(ac[0]+ac[1]);
-        hwe[0] = af>min_af ? -log(af*af) : -log(min_af*min_af);
-        hwe[1] = af>min_af && af<1-min_af ? -log(2*af*(1-af)) : -log(2*min_af*(1-min_af));
-        hwe[2] = af<(1-min_af) ? -log((1-af)*(1-af)) : -log(min_af*min_af);
+        // Calculate HWE probability for each possible qry+gt dosage combination. The alternate allele dosage
+        // values returned by gt_to_prob() below are 0,1,2,4 (0=missing, 1<<0, 1<<1, 1<<2). We consider only
+        // biallelic sites, therefore we work with eight genotype combinations.
+        //
+        // The array hwe_dsg is accessed with hwe_dsg[qry_dsg & gt_dsg] and is constructed to account for PL uncertainty
+        // when we encounter less informative PL, such as PL=0,0,10, where multiple dosage values are equally
+        // likely. If we allowed complete uncertainty (PL=0,0,0), we'd have up to eight possible genotype
+        // mask combinations: from e.g. 0=(gt_dsg=1<<0 & qry_dsg=1<<1) to 7=(gt_dsg=1<<0|1<<1|1<<2 & qry_dsg=1<<0|1<<1|1<<2).
+        // Note the extreme case of 1|2|4 is skipped, see pl_to_dsg().
+        //
+        // When the dosage is uncertain, we take the minimum of their corresponding HWE value, for example
+        //      hwe[0] = 0
+        //      hwe[1] = (1-AF)**2
+        //      hwe[2] = 2*AF*(1-AF)
+        //      hwe[4] = AF**2
+        //      hwe[3] = min{hwe[1],hwe[2]}
+
+        double hwe[3];  // while hwe_dsg iterates over dsg bitmasks (0..7), hwe iterates over dsg (0,1,2)
+        double af = ac[0]+ac[1] ? (double)ac[1]/(ac[0]+ac[1]) : 1e-6;
+        hwe[0] = -log((1-af)*(1-af));
+        hwe[1] = -log(2*af*(1-af));
+        hwe[2] = -log(af*af);
         hwe_dsg[0] = 0;
         for (i=1; i<8; i++)
         {
@@ -598,7 +660,7 @@ static void process_line(args_t *args)
     // The sample pairs were given explicitly via -p/-P options
     if ( args->pairs )
     {
-        if ( !args->use_PLs )
+        if ( !args->gt_err )
         {
             int ndiff = 0;
             if ( args->kbs_diff ) diff_sites_reset(args);
@@ -623,13 +685,17 @@ static void process_line(args_t *args)
                     args->ndiff[i]++;
                     if ( args->kbs_diff ) { ndiff++; kbs_insert(args->kbs_diff, i); }
                 }
-                else if ( args->calc_hwe_prob ) args->hwe_prob[i] += hwe_dsg[match];
+                else if ( args->calc_hwe_prob )
+                {
+                    args->hwe_prob[i] += hwe_dsg[match];
+                    args->nmatch[i]++;
+                }
                 args->ncnt[i]++;
             }
 
             if ( ndiff ) diff_sites_push(args, ndiff, qry_rec->rid, qry_rec->pos);
         }
-        else    // use_PLs set
+        else    // gt_err set
         {
             for (i=0; i<args->npairs; i++)
             {
@@ -657,6 +723,7 @@ static void process_line(args_t *args)
                 {
                     int match = qry_dsg & gt_dsg;
                     args->hwe_prob[i] += hwe_dsg[match];
+                    if ( match ) args->nmatch[i]++;
                 }
                 args->ncnt[i]++;
             }
@@ -665,7 +732,7 @@ static void process_line(args_t *args)
     }
 
     int idx=0;
-    if ( !args->use_PLs )
+    if ( !args->gt_err )
     {
         for (i=0; i<args->nqry_smpl; i++)
         {
@@ -692,13 +759,17 @@ static void process_line(args_t *args)
                 if ( !args->gt_dsg[j] ) { idx++; continue; }        // missing value
                 int match = args->qry_dsg[i] & args->gt_dsg[j];
                 if ( !match ) args->ndiff[idx]++;
-                else if ( args->calc_hwe_prob ) args->hwe_prob[idx] += hwe_dsg[match];
+                else if ( args->calc_hwe_prob )
+                {
+                    args->hwe_prob[idx] += hwe_dsg[match];
+                    args->nmatch[idx]++;
+                }
                 args->ncnt[idx]++;
                 idx++;
             }
         }
     }
-    else    // use_PLs set
+    else    // gt_err set
     {
         for (i=0; i<args->nqry_smpl; i++)
         {
@@ -733,6 +804,7 @@ static void process_line(args_t *args)
                 {
                     int match = args->qry_dsg[i] & args->gt_dsg[j];
                     args->hwe_prob[idx] += hwe_dsg[match];
+                    if ( match ) args->nmatch[idx]++;
                 }
                 args->ncnt[idx]++;
                 idx++;
@@ -760,12 +832,13 @@ static void report_distinctive_sites(args_t *args)
 {
     extsort_sort(args->es);
 
-    fprintf(args->fp,"# DS, distinctive sites:\n");
-    fprintf(args->fp,"#     - chromosome\n");
-    fprintf(args->fp,"#     - position\n");
-    fprintf(args->fp,"#     - cumulative number of pairs distinguished by this block\n");
-    fprintf(args->fp,"#     - block id\n");
-    fprintf(args->fp,"#DS\t[2]Chromosome\t[3]Position\t[4]Cumulative number of distinct pairs\t[5]Block id\n");
+    args->kstr.l = 0;
+    ksprintf(&args->kstr,"# DS, distinctive sites:\n");
+    ksprintf(&args->kstr,"#     - chromosome\n");
+    ksprintf(&args->kstr,"#     - position\n");
+    ksprintf(&args->kstr,"#     - cumulative number of pairs distinguished by this block\n");
+    ksprintf(&args->kstr,"#     - block id\n");
+    ksprintf(&args->kstr,"#DS\t[2]Chromosome\t[3]Position\t[4]Cumulative number of distinct pairs\t[5]Block id\n");
 
     kbitset_t *kbs_blk = kbs_init(args->npairs);
     kbitset_iter_t itr;
@@ -785,7 +858,9 @@ static void report_distinctive_sites(args_t *args)
         if ( ndiff_dbg!=ndiff ) error("Corrupted data, fixme: %d vs %d\n",ndiff_dbg,ndiff);
         if ( !ndiff_new ) continue;     // no new pair distinguished by this site
         ndiff_tot += ndiff_new;
-        fprintf(args->fp,"DS\t%s\t%d\t%d\t%d\n",bcf_hdr_id2name(args->qry_hdr,rid),pos+1,ndiff_tot,iblock);
+        args->kstr.l = 0;
+        ksprintf(&args->kstr,"DS\t%s\t%d\t%d\t%d\n",bcf_hdr_id2name(args->qry_hdr,rid),pos+1,ndiff_tot,iblock);
+        if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
         if ( ndiff_tot < ndiff_min ) continue;   // fewer than the requested number of pairs can be distinguished at this point
         iblock++;
         ndiff_tot = 0;
@@ -795,24 +870,35 @@ static void report_distinctive_sites(args_t *args)
 }
 static void report(args_t *args)
 {
-    fprintf(args->fp,"INFO\tsites-compared\t%u\n",args->ncmp);
-    fprintf(args->fp,"INFO\tsites-skipped-no-match\t%u\n",args->nskip_no_match);
-    fprintf(args->fp,"INFO\tsites-skipped-multiallelic\t%u\n",args->nskip_not_ba);
-    fprintf(args->fp,"INFO\tsites-skipped-monoallelic\t%u\n",args->nskip_mono);
-    fprintf(args->fp,"INFO\tsites-skipped-no-data\t%u\n",args->nskip_no_data);
-    fprintf(args->fp,"INFO\tsites-skipped-GT-not-diploid\t%u\n",args->nskip_dip_GT);
-    fprintf(args->fp,"INFO\tsites-skipped-PL-not-diploid\t%u\n",args->nskip_dip_PL);
-    fprintf(args->fp,"INFO\tsites-used-PL-vs-PL\t%u\n",args->nused[0][0]);
-    fprintf(args->fp,"INFO\tsites-used-PL-vs-GT\t%u\n",args->nused[0][1]);
-    fprintf(args->fp,"INFO\tsites-used-GT-vs-PL\t%u\n",args->nused[1][0]);
-    fprintf(args->fp,"INFO\tsites-used-GT-vs-GT\t%u\n",args->nused[1][1]);
-    fprintf(args->fp,"# DC, discordance:\n");
-    fprintf(args->fp,"#     - query sample\n");
-    fprintf(args->fp,"#     - genotyped sample\n");
-    fprintf(args->fp,"#     - discordance (either an abstract score or number of mismatches, see -e/-u in the man page for details; smaller is better)\n");
-    fprintf(args->fp,"#     - negative log of HWE probability at matching sites (rare genotypes matches are more informative, bigger is better)\n");
-    fprintf(args->fp,"#     - number of sites compared (bigger is better)\n");
-    fprintf(args->fp,"#DC\t[2]Query Sample\t[3]Genotyped Sample\t[4]Discordance\t[5]-log P(HWE)\t[6]Number of sites compared\n");
+    args->kstr.l = 0;
+    ksprintf(&args->kstr,"INFO\tsites-compared\t%u\n",args->ncmp);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-no-match\t%u\n",args->nskip_no_match);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-multiallelic\t%u\n",args->nskip_not_ba);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-monoallelic\t%u\n",args->nskip_mono);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-no-data\t%u\n",args->nskip_no_data);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-GT-not-diploid\t%u\n",args->nskip_dip_GT);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-PL-not-diploid\t%u\n",args->nskip_dip_PL);
+    ksprintf(&args->kstr,"INFO\tsites-skipped-filtering-expression\t%u\n",args->nskip_filter);
+    ksprintf(&args->kstr,"INFO\tsites-used-PL-vs-PL\t%u\n",args->nused[0][0]);
+    ksprintf(&args->kstr,"INFO\tsites-used-PL-vs-GT\t%u\n",args->nused[0][1]);
+    ksprintf(&args->kstr,"INFO\tsites-used-GT-vs-PL\t%u\n",args->nused[1][0]);
+    ksprintf(&args->kstr,"INFO\tsites-used-GT-vs-GT\t%u\n",args->nused[1][1]);
+    ksprintf(&args->kstr,"# DCv2, discordance version 2:\n");
+    ksprintf(&args->kstr,"#     - Query sample\n");
+    ksprintf(&args->kstr,"#     - Genotyped sample\n");
+    ksprintf(&args->kstr,"#     - Discordance, given either as an abstract score or number of mismatches, see the options -E/-u\n"
+                         "#       in man page for details. Note that samples with high missingness have fewer sites compared,\n"
+                         "#       which results in lower overall discordance. Therefore it is advisable to use the average score\n"
+                         "#       per site rather than the absolute value, i.e. divide the value by the number of sites compared\n"
+                         "#       (smaller value = better match)\n");
+    ksprintf(&args->kstr,"#     - Average negative log of HWE probability at matching sites, attempts to quantify the following\n"
+                         "#       intuition: rare genotype matches are more informative than common genotype matches, hence two\n"
+                         "#       samples with similar discordance can be further stratified by the HWE score (bigger value = better\n"
+                         "#       match, the observed concordance was less likely to occur by chance)\n");
+    ksprintf(&args->kstr,"#     - Number of sites compared for this pair of samples (bigger = more informative)\n");
+    ksprintf(&args->kstr,"#     - Number of matching genotypes\n");
+    ksprintf(&args->kstr,"#DCv2\t[2]Query Sample\t[3]Genotyped Sample\t[4]Discordance\t[5]Average -log P(HWE)\t[6]Number of sites compared\t[7]Number of matching genotypes\n");
+    if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
 
     int trim = args->ntop;
     if ( !args->pairs )
@@ -826,26 +912,30 @@ static void report(args_t *args)
         int i;
         for (i=0; i<args->npairs; i++)
         {
+            args->kstr.l = 0;
             int iqry = args->pairs[i].iqry;
             int igt  = args->pairs[i].igt;
             if ( args->ndiff )
             {
-                fprintf(args->fp,"DC\t%s\t%s\t%u\t%e\t%u\n",
+                ksprintf(&args->kstr,"DCv2\t%s\t%s\t%u\t%e\t%u\t%u\n",
                         args->qry_hdr->samples[iqry],
                         args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                         args->ndiff[i],
-                        args->calc_hwe_prob ? args->hwe_prob[i] : 0,
-                        args->ncnt[i]);
+                        (args->calc_hwe_prob && args->nmatch[i]) ? args->hwe_prob[i]/args->nmatch[i] : 0,
+                        args->ncnt[i],
+                        args->nmatch[i]);
             }
             else
             {
-                fprintf(args->fp,"DC\t%s\t%s\t%e\t%e\t%u\n",
+                ksprintf(&args->kstr,"DCv2\t%s\t%s\t%e\t%e\t%u\t%u\n",
                         args->qry_hdr->samples[iqry],
                         args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                         args->pdiff[i],
-                        args->calc_hwe_prob ? args->hwe_prob[i] : 0,
-                        args->ncnt[i]);
+                        (args->calc_hwe_prob && args->nmatch[i]) ? args->hwe_prob[i]/args->nmatch[i] : 0,
+                        args->ncnt[i],
+                        args->nmatch[i]);
             }
+            if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
         }
     }
     else if ( !trim )
@@ -857,25 +947,29 @@ static void report(args_t *args)
             int ngt  = args->cross_check ? i : args->ngt_smpl;
             for (j=0; j<ngt; j++)
             {
+                args->kstr.l = 0;
                 int igt = args->gt_smpl ? args->gt_smpl[j] : j;
                 if ( args->ndiff )
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%u\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%u\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                             args->ndiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
                 else
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%e\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%e\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                             args->pdiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
+                if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
                 idx++;
             }
         }
@@ -890,7 +984,7 @@ static void report(args_t *args)
             for (j=0; j<args->ngt_smpl; j++)
             {
                 if ( args->sort_by_hwe )
-                    arr[j].val = -args->hwe_prob[idx];
+                    arr[j].val = args->nmatch[idx] ? -args->hwe_prob[idx]/args->nmatch[idx] : 0;  // -args->hwe_prob[idx];
                 else if ( args->ndiff )
                     arr[j].val = args->ncnt[idx] ? (double)args->ndiff[idx]/args->ncnt[idx] : 0;
                 else
@@ -903,26 +997,30 @@ static void report(args_t *args)
             int iqry = args->qry_smpl ? args->qry_smpl[i] : i;
             for (j=0; j<args->ntop; j++)
             {
+                args->kstr.l = 0;
                 int idx = arr[j].idx;
                 int igt = args->gt_smpl ? args->gt_smpl[arr[j].ism] : arr[j].ism;
                 if ( args->ndiff )
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%u\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%u\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                             args->ndiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
                 else
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%e\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%e\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->gt_hdr?args->gt_hdr->samples[igt]:args->qry_hdr->samples[igt],
                             args->pdiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
+                if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
             }
         }
         free(arr);
@@ -938,7 +1036,7 @@ static void report(args_t *args)
             for (j=0; j<i; j++)
             {
                 if ( args->sort_by_hwe )
-                    arr[k].val = -args->hwe_prob[idx];
+                    arr[k].val = args->nmatch[idx] ? -args->hwe_prob[idx]/args->nmatch[idx] : 0;
                 else if ( args->ndiff )
                     arr[k].val = args->ncnt[idx] ? (double)args->ndiff[idx]/args->ncnt[idx] : 0;
                 else
@@ -952,7 +1050,7 @@ static void report(args_t *args)
             {
                 idx = j*(j+1)/2 + i;
                 if ( args->sort_by_hwe )
-                    arr[k].val = -args->hwe_prob[idx];
+                    arr[k].val = args->nmatch[idx] ? -args->hwe_prob[idx]/args->nmatch[idx] : 0;
                 else if ( args->ndiff )
                     arr[k].val = args->ncnt[idx] ? (double)args->ndiff[idx]/args->ncnt[idx] : 0;
                 else
@@ -965,27 +1063,31 @@ static void report(args_t *args)
             int iqry = args->qry_smpl ? args->qry_smpl[i] : i;
             for (j=0; j<args->ntop; j++)
             {
+                args->kstr.l = 0;
                 if ( i <= arr[j].ism ) continue;
                 int idx = arr[j].idx;
                 int igt = args->qry_smpl ? args->qry_smpl[arr[j].ism] : arr[j].ism;
                 if ( args->ndiff )
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%u\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%u\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->qry_hdr->samples[igt],
                             args->ndiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
                 else
                 {
-                    fprintf(args->fp,"DC\t%s\t%s\t%e\t%e\t%u\n",
+                    ksprintf(&args->kstr,"DCv2\t%s\t%s\t%e\t%e\t%u\t%u\n",
                             args->qry_hdr->samples[iqry],
                             args->qry_hdr->samples[igt],
                             args->pdiff[idx],
-                            args->calc_hwe_prob ? args->hwe_prob[idx] : 0,
-                            args->ncnt[idx]);
+                            (args->calc_hwe_prob && args->nmatch[idx]) ? args->hwe_prob[idx]/args->nmatch[idx] : 0,
+                            args->ncnt[idx],
+                            args->calc_hwe_prob ? args->nmatch[idx] : 0);
                 }
+                if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
             }
         }
         free(arr);
@@ -1055,12 +1157,16 @@ static void usage(void)
     fprintf(bcftools_stderr, "                                           and TMP is a prefix of temporary files used by external sorting [/tmp/bcftools.XXXXXX]\n");
 #endif
     fprintf(bcftools_stderr, "        --dry-run                      Stop after first record to estimate required time\n");
-    fprintf(bcftools_stderr, "    -e, --error-probability INT        Phred-scaled probability of genotyping error, 0 for faster but less accurate results [40]\n");
+    fprintf(bcftools_stderr, "    -E, --error-probability INT        Phred-scaled probability of genotyping error, 0 for faster but less accurate results [40]\n");
+    fprintf(bcftools_stderr, "    -e, --exclude [qry|gt]:EXPR        Exclude sites for which the expression is true\n");
     fprintf(bcftools_stderr, "    -g, --genotypes FILE               Genotypes to compare against\n");
     fprintf(bcftools_stderr, "    -H, --homs-only                    Homozygous genotypes only, useful with low coverage data (requires -g)\n");
+    fprintf(bcftools_stderr, "    -i, --include [qry|gt]:EXPR        Include sites for which the expression is true\n");
     fprintf(bcftools_stderr, "        --n-matches INT                Print only top INT matches for each sample (sorted by average score), 0 for unlimited.\n");
     fprintf(bcftools_stderr, "                                           Use negative value to sort by HWE probability rather than by discordance [0]\n");
     fprintf(bcftools_stderr, "        --no-HWE-prob                  Disable calculation of HWE probability\n");
+    fprintf(bcftools_stderr, "    -o, --output FILE                  Write output to a file [standard output]\n");
+    fprintf(bcftools_stderr, "    -O, --output-type t|z              t: plain tab-delimited text output, z: compressed [t]\n");
     fprintf(bcftools_stderr, "    -p, --pairs LIST                   Comma-separated sample pairs to compare (qry,gt[,qry,gt..] with -g or qry,qry[,qry,qry..] w/o)\n");
     fprintf(bcftools_stderr, "    -P, --pairs-file FILE              File with tab-delimited sample pairs to compare (qry,gt with -g or qry,qry w/o)\n");
     fprintf(bcftools_stderr, "    -r, --regions REGION               Restrict to comma-separated list of regions\n");
@@ -1073,10 +1179,10 @@ static void usage(void)
     fprintf(bcftools_stderr, "        --targets-overlap 0|1|2        Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n");
     fprintf(bcftools_stderr, "    -u, --use TAG1[,TAG2]              Which tag to use in the query file (TAG1) and the -g file (TAG2) [PL,GT]\n");
     fprintf(bcftools_stderr, "Examples:\n");
-    fprintf(bcftools_stderr, "   # Check discordance of all samples from B against all sample in A\n");
+    fprintf(bcftools_stderr, "   # Check discordance of all samples from B against all samples in A\n");
     fprintf(bcftools_stderr, "   bcftools gtcheck -g A.bcf B.bcf\n");
     fprintf(bcftools_stderr, "\n");
-    fprintf(bcftools_stderr, "   # Limit comparisons to the fiven list of samples\n");
+    fprintf(bcftools_stderr, "   # Limit comparisons to the given list of samples\n");
     fprintf(bcftools_stderr, "   bcftools gtcheck -s gt:a1,a2,a3 -s qry:b1,b2 -g A.bcf B.bcf\n");
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "   # Compare only two pairs a1,b1 and a1,b2\n");
@@ -1093,9 +1199,10 @@ int main_vcfgtcheck(int argc, char *argv[])
     args->qry_use_GT = -1;
     args->gt_use_GT  = -1;
     args->calc_hwe_prob = 1;
-    args->use_PLs = 40;
+    args->gt_err = 40;
     args->regions_overlap = 1;
     args->targets_overlap = 0;
+    args->output_fname = "-";
 
     // external sort for --distinctive-sites
 #ifdef _WIN32
@@ -1114,7 +1221,11 @@ int main_vcfgtcheck(int argc, char *argv[])
 
     static struct option loptions[] =
     {
-        {"error-probability",1,0,'e'},
+        {"error-probability",1,0,'E'},  // note this used to be 'e', but can easily auto-detect to assure backward compatibility
+        {"exclude",required_argument,0,'e'},
+        {"include",required_argument,0,'i'},
+        {"output",required_argument,0,'o'},
+        {"output-type",required_argument,NULL,'O'},
         {"use",1,0,'u'},
         {"cluster",1,0,'c'},
         {"GTs-only",1,0,'G'},
@@ -1141,10 +1252,79 @@ int main_vcfgtcheck(int argc, char *argv[])
         {0,0,0,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "hg:p:s:S:p:P:Hr:R:at:T:G:c:u:e:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hg:p:s:S:p:P:Hr:R:at:T:G:c:u:e:E:i:o:O:",loptions,NULL)) >= 0) {
         switch (c) {
+            case 'o': args->output_fname = optarg; break;
+            case 'O':
+                switch (optarg[0]) {
+                    case 't': args->output_type = FT_TAB_TEXT; break;
+                    case 'z': args->output_type = FT_VCF_GZ; break;
+                    default:
+                    {
+                        args->clevel = strtol(optarg,&tmp,10);
+                        if ( *tmp || args->clevel<0 || args->clevel>9 ) error("The output type \"%s\" not recognised\n", optarg);
+                    }
+                }
+                if ( optarg[1] )
+                {
+                    args->clevel = strtol(optarg+1,&tmp,10);
+                    if ( *tmp || args->clevel<0 || args->clevel>9 ) error("Could not parse argument: --output-type %s\n", optarg+1);
+                }
+                break;
             case 'e':
-                args->use_PLs = strtol(optarg,&tmp,10);
+                if ( !strncasecmp("gt:",optarg,3) )
+                {
+                    if ( args->gt_filter_str ) error("Error: only one -i or -e expression can be given for gt:, and they cannot be combined\n");
+                    args->gt_filter_str = optarg;
+                    args->gt_filter_logic |= FLT_EXCLUDE;
+                }
+                else if ( !strncasecmp("qry:",optarg,4) )
+                {
+                    if ( args->qry_filter_str ) error("Error: only one -i or -e expression can be given for qry:, and they cannot be combined\n");
+                    args->qry_filter_str = optarg;
+                    args->qry_filter_logic |= FLT_EXCLUDE;
+                }
+                else
+                {
+                    // this could be the old -e, --error-probability option
+                    args->gt_err = strtol(optarg,&tmp,10);
+                    if ( !tmp || *tmp )
+                    {
+                        // it is not
+                        args->gt_filter_str  = optarg;
+                        args->qry_filter_str = optarg;
+                        args->gt_filter_logic  |= FLT_EXCLUDE;
+                        args->qry_filter_logic |= FLT_EXCLUDE;
+                    }
+                    else
+                    {
+                        fprintf(bcftools_stderr,"[warning] auto-detected the old format --error-probability option, please switch from -e to -E.\n");
+                    }
+                }
+                break;
+            case 'i':
+                if ( !strncasecmp("gt:",optarg,3) )
+                {
+                    if ( args->gt_filter_str ) error("Error: only one -i or -e expression can be given for gt:, and they cannot be combined\n");
+                    args->gt_filter_str = optarg;
+                    args->gt_filter_logic |= FLT_INCLUDE;
+                }
+                else if ( !strncasecmp("qry:",optarg,4) )
+                {
+                    if ( args->qry_filter_str ) error("Error: only one -i or -e expression can be given for qry:, and they cannot be combined\n");
+                    args->qry_filter_str = optarg;
+                    args->qry_filter_logic |= FLT_INCLUDE;
+                }
+                else
+                {
+                    args->gt_filter_str  = optarg;
+                    args->qry_filter_str = optarg;
+                    args->gt_filter_logic  |= FLT_INCLUDE;
+                    args->qry_filter_logic |= FLT_INCLUDE;
+                }
+                break;
+            case 'E':
+                args->gt_err = strtol(optarg,&tmp,10);
                 if ( !tmp || *tmp ) error("Could not parse: --error-probability %s\n", optarg);
                 break;
             case 'u':
@@ -1189,7 +1369,7 @@ int main_vcfgtcheck(int argc, char *argv[])
                     while ( *tmp && *tmp!=',' ) tmp++;
                     if ( *tmp ) { *tmp = 0; args->es_tmp_prefix = tmp+1; }
                 }
-                args->use_PLs = 0;
+                args->gt_err = 0;
                 break;
             case 'c':
                 error("The -c option is to be implemented, please open an issue on github\n");
@@ -1249,7 +1429,7 @@ int main_vcfgtcheck(int argc, char *argv[])
     }
     if ( args->distinctive_sites && !args->pair_samples ) error("The experimental option --distinctive-sites requires -p/-P\n");
     if ( args->hom_only && !args->gt_fname ) error("The option --homs-only requires --genotypes\n");
-    if ( args->distinctive_sites && args->use_PLs ) error("The option --distinctive-sites cannot be combined with --error-probability\n");
+    if ( args->distinctive_sites && args->gt_err ) error("The option --distinctive-sites cannot be combined with --error-probability\n");
 
     init_data(args);
 
@@ -1269,7 +1449,9 @@ int main_vcfgtcheck(int argc, char *argv[])
             gettimeofday(&t1, NULL);
             double delta = (t1.tv_sec - t0.tv_sec) * 1e6 + (t1.tv_usec - t0.tv_usec);
             fprintf(bcftools_stderr,"INFO:\tTime required to process one record .. %f seconds\n",delta/1e6);
-            fprintf(args->fp,"INFO\tTime required to process one record .. %f seconds\n",delta/1e6);
+            args->kstr.l = 0;
+            ksprintf(&args->kstr,"INFO\tTime required to process one record .. %f seconds\n",delta/1e6);
+            if ( bgzf_write(args->out_fh, args->kstr.s, args->kstr.l)!=args->kstr.l ) error("Failed to write to %s\n", args->output_fname);
             if ( args->dry_run ) break;
         }
     }
index 20be2a94701f419bc122dbad436f37016f80f29f..0b0222b521ba983f562d1e47bdd470684b9328f1 100644 (file)
@@ -1,6 +1,7 @@
 /*  vcfhead.c -- view VCF/BCF file headers.
 
     Copyright (C) 2021 University of Glasgow.
+    Copyright (C) 2023 Genome Research Ltd.
 
     Author: John Marshall <jmarshall@hey.com>
 
@@ -41,30 +42,36 @@ int main_vcfhead(int argc, char *argv[])
 "Usage: bcftools head [OPTION]... [FILE]\n"
 "\n"
 "Options:\n"
-"  -h, --headers INT   Display INT header lines [all]\n"
-"  -n, --records INT   Display INT variant record lines [none]\n"
+"  -h, --headers INT    Display INT header lines [all]\n"
+"  -n, --records INT    Display INT variant record lines [none]\n"
+"  -s, --samples INT    Display INT records starting with the #CHROM header line [none]\n"
 "\n";
 
     static const struct option loptions[] = {
         { "headers", required_argument, NULL, 'h' },
         { "records", required_argument, NULL, 'n' },
+        { "samples", required_argument, NULL, 's' },
         { NULL, 0, NULL, 0 }
     };
 
     int all_headers = 1;
+    int samples = 0;
     uint64_t nheaders = 0;
     uint64_t nrecords = 0;
 
     int c, nargs;
-    while ((c = getopt_long(argc, argv, "h:n:", loptions, NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h:n:s:", loptions, NULL)) >= 0)
         switch (c) {
         case 'h': all_headers = 0; nheaders = strtoull(optarg, NULL, 0); break;
         case 'n': nrecords = strtoull(optarg, NULL, 0); break;
+        case 's': nrecords = strtoull(optarg, NULL, 0); samples = 1; break;
         default:
             fputs(usage, stderr);
             return EXIT_FAILURE;
         }
 
+    if ( samples && all_headers ) all_headers = 0;
+
     nargs = argc - optind;
     if (nargs == 0 && isatty(STDIN_FILENO)) {
         fputs(usage, stdout);
@@ -99,17 +106,34 @@ int main_vcfhead(int argc, char *argv[])
         bcf_hdr_format(hdr, 0, &str);
         fputs(ks_str(&str), stdout);
     }
-    else if (nheaders > 0) {
+    else if (nheaders > 0 || samples ) {
         bcf_hdr_format(hdr, 0, &str);
         char *lim = str.s;
         uint64_t n;
+        int samples_printed = 0;
         for (n = 0; n < nheaders; n++) {
+            if ( samples && !strncmp(lim,"#CHROM\t",7) ) samples_printed = 1;
             lim = strchr(lim, '\n');
             if (lim) lim++;
             else break;
         }
-        if (lim) *lim = '\0';
-        fputs(ks_str(&str), stdout);
+        if ( nheaders )
+        {
+            char tmp;
+            if (lim) { tmp = *lim; *lim = '\0'; }
+            fputs(ks_str(&str), stdout);
+            if (lim) *lim = tmp;
+        }
+        if ( lim && samples && !samples_printed )
+        {
+            while ( lim && *lim )
+            {
+                if ( !strncmp(lim,"#CHROM\t",7) ) { fputs(lim, stdout); break; }
+                lim = strchr(lim, '\n');
+                if (lim) lim++;
+                else break;
+            }
+        }
     }
 
     if (nrecords > 0) {
index 09744f23e3d0604b66ce246667e2d28f9f300513..832c9bd747aa83a92ca9d85733a6f46f1ddc720e 100644 (file)
@@ -3,6 +3,7 @@
 /*  vcfhead.c -- view VCF/BCF file headers.
 
     Copyright (C) 2021 University of Glasgow.
+    Copyright (C) 2023 Genome Research Ltd.
 
     Author: John Marshall <jmarshall@hey.com>
 
@@ -43,30 +44,36 @@ int main_vcfhead(int argc, char *argv[])
 "Usage: bcftools head [OPTION]... [FILE]\n"
 "\n"
 "Options:\n"
-"  -h, --headers INT   Display INT header lines [all]\n"
-"  -n, --records INT   Display INT variant record lines [none]\n"
+"  -h, --headers INT    Display INT header lines [all]\n"
+"  -n, --records INT    Display INT variant record lines [none]\n"
+"  -s, --samples INT    Display INT records starting with the #CHROM header line [none]\n"
 "\n";
 
     static const struct option loptions[] = {
         { "headers", required_argument, NULL, 'h' },
         { "records", required_argument, NULL, 'n' },
+        { "samples", required_argument, NULL, 's' },
         { NULL, 0, NULL, 0 }
     };
 
     int all_headers = 1;
+    int samples = 0;
     uint64_t nheaders = 0;
     uint64_t nrecords = 0;
 
     int c, nargs;
-    while ((c = getopt_long(argc, argv, "h:n:", loptions, NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h:n:s:", loptions, NULL)) >= 0)
         switch (c) {
         case 'h': all_headers = 0; nheaders = strtoull(optarg, NULL, 0); break;
         case 'n': nrecords = strtoull(optarg, NULL, 0); break;
+        case 's': nrecords = strtoull(optarg, NULL, 0); samples = 1; break;
         default:
             fputs(usage, bcftools_stderr);
             return EXIT_FAILURE;
         }
 
+    if ( samples && all_headers ) all_headers = 0;
+
     nargs = argc - optind;
     if (nargs == 0 && isatty(STDIN_FILENO)) {
         fputs(usage, bcftools_stdout);
@@ -101,17 +108,34 @@ int main_vcfhead(int argc, char *argv[])
         bcf_hdr_format(hdr, 0, &str);
         fputs(ks_str(&str), bcftools_stdout);
     }
-    else if (nheaders > 0) {
+    else if (nheaders > 0 || samples ) {
         bcf_hdr_format(hdr, 0, &str);
         char *lim = str.s;
         uint64_t n;
+        int samples_printed = 0;
         for (n = 0; n < nheaders; n++) {
+            if ( samples && !strncmp(lim,"#CHROM\t",7) ) samples_printed = 1;
             lim = strchr(lim, '\n');
             if (lim) lim++;
             else break;
         }
-        if (lim) *lim = '\0';
-        fputs(ks_str(&str), bcftools_stdout);
+        if ( nheaders )
+        {
+            char tmp;
+            if (lim) { tmp = *lim; *lim = '\0'; }
+            fputs(ks_str(&str), bcftools_stdout);
+            if (lim) *lim = tmp;
+        }
+        if ( lim && samples && !samples_printed )
+        {
+            while ( lim && *lim )
+            {
+                if ( !strncmp(lim,"#CHROM\t",7) ) { fputs(lim, bcftools_stdout); break; }
+                lim = strchr(lim, '\n');
+                if (lim) lim++;
+                else break;
+            }
+        }
     }
 
     if (nrecords > 0) {
index 1dd960ea789008368acc397988a468783826ebbf..17eac5f323a3c0adb9a63c4668c2884e38fba05c 100644 (file)
@@ -1,6 +1,6 @@
 /*  vcfindex.c -- Index bgzip compressed VCF/BCF files for random access.
 
-    Copyright (C) 2014-2021 Genome Research Ltd.
+    Copyright (C) 2014-2024 Genome Research Ltd.
 
     Author: Shane McCarthy <sm15@sanger.ac.uk>
 
@@ -264,6 +264,7 @@ int main_vcfindex(int argc, char *argv[])
             default: usage();
         }
     }
+    if (!min_shift) tbi = 1;
     if (stats > total)
     {
         fprintf(stderr, "[E::%s] expected only one of --stats or --nrecords options\n", __func__);
index ac9e3ba2b6a1d267474ee99c376f1ae48a5ee399..8f6932f1f1203a65d557742e20666c9b3afcb6ac 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  vcfindex.c -- Index bgzip compressed VCF/BCF files for random access.
 
-    Copyright (C) 2014-2021 Genome Research Ltd.
+    Copyright (C) 2014-2024 Genome Research Ltd.
 
     Author: Shane McCarthy <sm15@sanger.ac.uk>
 
@@ -266,6 +266,7 @@ int main_vcfindex(int argc, char *argv[])
             default: usage();
         }
     }
+    if (!min_shift) tbi = 1;
     if (stats > total)
     {
         fprintf(bcftools_stderr, "[E::%s] expected only one of --stats or --nrecords options\n", __func__);
index 4ee29b4c87f92ed843bb8a7eb1bff4dcf68366cb..24a45685b083b82e63c101e1207aec92eb7dd071 100644 (file)
@@ -34,6 +34,7 @@ THE SOFTWARE.  */
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
 #include <htslib/hts_os.h>
+#include <htslib/hts_defs.h>
 #include "bcftools.h"
 #include "filter.h"
 
@@ -58,7 +59,7 @@ typedef struct
     FILE *fh_log, *fh_sites;
     htsFile **fh_out;
     char **argv, *prefix, *output_fname, **fnames, *write_files, *targets_list, *regions_list;
-    char *isec_exact;
+    char *isec_exact, *file_list;
     int argc, record_cmd_line;
     char *index_fn;
     int write_index;
@@ -69,19 +70,21 @@ args_t;
  *  mkdir_p() - create new directory for a file $fname
  *  @fname:   the file name to create the directory for, the part after last "/" is ignored
  */
-void mkdir_p(const char *fmt, ...)
+void HTS_FORMAT(HTS_PRINTF_FMT, 1, 2)
+mkdir_p(const char *fmt, ...)
 {
     va_list ap;
     va_start(ap, fmt);
     int n = vsnprintf(NULL, 0, fmt, ap) + 2;
     va_end(ap);
 
-    char *path = (char*)malloc(n);
+    char *tmp = (char*)malloc(n);
+    if (!tmp) error("Couldn't allocate space for path: %s\n", strerror(errno));
     va_start(ap, fmt);
-    vsnprintf(path, n, fmt, ap);
+    vsnprintf(tmp, n, fmt, ap);
     va_end(ap);
 
-    char *tmp = strdup(path), *p = tmp+1;
+    char *p = tmp+1;
     while (*p)
     {
         while (*p && *p!='/') p++;
@@ -89,12 +92,11 @@ void mkdir_p(const char *fmt, ...)
         char ctmp = *p;
         *p = 0;
         int ret = mkdir(tmp,S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
-        if ( ret!=0 && errno!=EEXIST ) error("Error creating directory %s: %s\n", path,strerror(errno));
+        if ( ret!=0 && errno!=EEXIST ) error("Error creating directory %s: %s\n", tmp,strerror(errno));
         *p = ctmp;
         while ( *p && *p=='/' ) p++;
     }
     free(tmp);
-    free(path);
 }
 
 /**
@@ -105,7 +107,8 @@ void mkdir_p(const char *fmt, ...)
  *
  *  Returns open file descriptor or NULL if mode is NULL.
  */
-FILE *open_file(char **fname, const char *mode, const char *fmt, ...)
+FILE * HTS_FORMAT(HTS_PRINTF_FMT, 3, 4)
+open_file(char **fname, const char *mode, const char *fmt, ...)
 {
     va_list ap;
     va_start(ap, fmt);
@@ -117,7 +120,7 @@ FILE *open_file(char **fname, const char *mode, const char *fmt, ...)
     vsnprintf(str, n, fmt, ap);
     va_end(ap);
 
-    mkdir_p(str);
+    mkdir_p("%s", str);
     if ( !mode )
     {
         if ( !fname ) error("Uh: expected fname or mode\n");
@@ -150,8 +153,11 @@ void isec_vcf(args_t *args)
         if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
         if (args->record_cmd_line) bcf_hdr_append_version(files->readers[args->iwrite].header,args->argc,args->argv,"bcftools_isec");
         if ( bcf_hdr_write(out_fh, files->readers[args->iwrite].header)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname?args->output_fname:"standard output");
-        if ( args->write_index && init_index(out_fh,files->readers[args->iwrite].header,args->output_fname,&args->index_fn)<0 )
-            error("Error: failed to initialise index for %s\n",args->output_fname?args->output_fname:"standard output");
+        if ( init_index2(out_fh,files->readers[args->iwrite].header,
+                         args->output_fname,&args->index_fn,
+                         args->write_index)<0 )
+            error("Error: failed to initialise index for %s\n",
+                  args->output_fname?args->output_fname:"standard output");
     }
     if ( !args->nwrite && !out_std && !args->prefix )
         fprintf(stderr,"Note: -w option not given, printing list of sites...\n");
@@ -454,12 +460,14 @@ static void destroy_data(args_t *args)
         {
             if ( !args->fnames[i] ) continue;
             if ( hts_close(args->fh_out[i])!=0 ) error("[%s] Error: close failed .. %s\n", __func__,args->fnames[i]);
-            if ( args->output_type==FT_VCF_GZ )
+            int is_tbi = !args->write_index 
+                      || (args->write_index&127) == HTS_FMT_TBI;
+            if ( args->output_type==FT_VCF_GZ && is_tbi )
             {
                 tbx_conf_t conf = tbx_conf_vcf;
                 tbx_index_build(args->fnames[i], -1, &conf);
             }
-            else if ( args->output_type==FT_BCF_GZ )
+            else if ( args->output_type==FT_BCF_GZ || !is_tbi )
             {
                 if ( bcf_index_build(args->fnames[i],14) ) error("Could not index %s\n", args->fnames[i]);
             }
@@ -484,6 +492,7 @@ static void usage(void)
     fprintf(stderr, "    -e, --exclude EXPR             Exclude sites for which the expression is true\n");
     fprintf(stderr, "    -f, --apply-filters LIST       Require at least one of the listed FILTER strings (e.g. \"PASS,.\")\n");
     fprintf(stderr, "    -i, --include EXPR             Include only sites for which the expression is true\n");
+    fprintf(stderr, "    -l, --file-list FILE           Read the input file names from the file\n");
     fprintf(stderr, "        --no-version               Do not append version and command line to the header\n");
     fprintf(stderr, "    -n, --nfiles [+-=~]INT         Output positions present in this many (=), this many or more (+), this many or fewer (-), the exact (~) files\n");
     fprintf(stderr, "    -o, --output FILE              Write output to a file [standard output]\n");
@@ -497,7 +506,7 @@ static void usage(void)
     fprintf(stderr, "        --targets-overlap 0|1|2    Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n");
     fprintf(stderr, "        --threads INT              Use multithreading with <int> worker threads [0]\n");
     fprintf(stderr, "    -w, --write LIST               List of files to write with -p given as 1-based indexes. By default, all files are written\n");
-    fprintf(stderr, "        --write-index              Automatically index the output files [off]\n");
+    fprintf(stderr, "    -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     fprintf(stderr, "Examples:\n");
     fprintf(stderr, "   # Create intersection and complements of two sets saving the output in dir/*\n");
@@ -541,6 +550,7 @@ int main_vcfisec(int argc, char *argv[])
         {"collapse",required_argument,NULL,'c'},
         {"complement",no_argument,NULL,'C'},
         {"apply-filters",required_argument,NULL,'f'},
+        {"file-list",required_argument,NULL,'l'},
         {"nfiles",required_argument,NULL,'n'},
         {"prefix",required_argument,NULL,'p'},
         {"write",required_argument,NULL,'w'},
@@ -554,11 +564,11 @@ int main_vcfisec(int argc, char *argv[])
         {"output-type",required_argument,NULL,'O'},
         {"threads",required_argument,NULL,9},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,10},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "hc:r:R:p:n:w:t:T:Cf:o:O:i:e:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hc:r:R:p:n:w:t:T:Cf:o:O:i:e:l:W::",loptions,NULL)) >= 0) {
         switch (c) {
             case 'o': args->output_fname = optarg; break;
             case 'O':
@@ -593,12 +603,16 @@ int main_vcfisec(int argc, char *argv[])
             case 'C':
                 if ( args->isec_op!=0 && args->isec_op!=OP_COMPLEMENT ) error("Error: either -C or -n should be given, not both.\n");
                 args->isec_op = OP_COMPLEMENT; break;
+            case 'l': args->file_list = optarg; break;
             case 'r': args->regions_list = optarg; break;
             case 'R': args->regions_list = optarg; regions_is_file = 1; break;
             case 't': args->targets_list = optarg; break;
             case 'T': args->targets_list = optarg; targets_is_file = 1; break;
             case 'p': args->prefix = optarg; break;
-            case 'w': args->write_files = optarg; break;
+            case 'w':
+                if ( args->write_files ) error("The option -w accepts a list of indices and can be given only once\n");
+                args->write_files = optarg;
+                break;
             case 'i': add_filter(args, optarg, FLT_INCLUDE); break;
             case 'e': add_filter(args, optarg, FLT_EXCLUDE); break;
             case 'n':
@@ -626,13 +640,33 @@ int main_vcfisec(int argc, char *argv[])
                 break;
             case  9 : args->n_threads = strtol(optarg, 0, 0); break;
             case  8 : args->record_cmd_line = 0; break;
-            case 10 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'h':
             case '?': usage(); break;
             default: error("Unknown argument: %s\n", optarg);
         }
     }
-    if ( argc-optind<1 ) usage();   // no file given
+    if ( argc-optind<1 && !args->file_list ) usage();   // no file given
+
+    int nfiles = 0,i;
+    char **files = NULL;
+    if ( args->file_list )
+    {
+        files = hts_readlines(args->file_list, &nfiles);
+        if ( !files ) error("Failed to read from %s\n", args->file_list);
+    }
+    if ( optind<argc )
+    {
+        int n = argc - optind;
+        files = (char**)realloc(files,sizeof(*files)*(n+nfiles));
+        for (i=nfiles; i>0; i--) files[n+i-1] = files[n+i-2];
+        for (i=0; i<n; i++) files[i] = strdup(argv[optind+i]);
+        nfiles += n;
+    }
+
     if ( args->targets_list )
     {
         bcf_sr_set_opt(args->files,BCF_SR_TARGETS_OVERLAP,targets_overlap);
@@ -645,7 +679,7 @@ int main_vcfisec(int argc, char *argv[])
         if ( bcf_sr_set_regions(args->files, args->regions_list, regions_is_file)<0 )
             error("Failed to read the regions: %s\n", args->regions_list);
     }
-    if ( argc-optind==2 && !args->isec_op )
+    if ( nfiles==2 && !args->isec_op )
     {
         args->isec_op = OP_VENN;
         if ( !args->prefix ) error("Expected the -p option\n");
@@ -656,11 +690,13 @@ int main_vcfisec(int argc, char *argv[])
         args->isec_n  = 1;
     }
     args->files->require_index = 1;
-    while (optind<argc)
+    for (i=0; i<nfiles; i++)
     {
-        if ( !bcf_sr_add_reader(args->files, argv[optind]) ) error("Failed to open %s: %s\n", argv[optind],bcf_sr_strerror(args->files->errnum));
-        optind++;
+        if ( !bcf_sr_add_reader(args->files, files[i]) ) error("Failed to open %s: %s\n", files[i],bcf_sr_strerror(args->files->errnum));
+        free(files[i]);
     }
+    free(files);
+
     init_data(args);
     isec_vcf(args);
     destroy_data(args);
index 76e4d3a9f57e13c6c7fcfb3dbda7da572c9cf86b..f4727c1d3ca219bc0bda7bea7faba9ea1d33b566 100644 (file)
@@ -36,6 +36,7 @@ THE SOFTWARE.  */
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
 #include <htslib/hts_os.h>
+#include <htslib/hts_defs.h>
 #include "bcftools.h"
 #include "filter.h"
 
@@ -60,7 +61,7 @@ typedef struct
     FILE *fh_log, *fh_sites;
     htsFile **fh_out;
     char **argv, *prefix, *output_fname, **fnames, *write_files, *targets_list, *regions_list;
-    char *isec_exact;
+    char *isec_exact, *file_list;
     int argc, record_cmd_line;
     char *index_fn;
     int write_index;
@@ -71,19 +72,21 @@ args_t;
  *  mkdir_p() - create new directory for a file $fname
  *  @fname:   the file name to create the directory for, the part after last "/" is ignored
  */
-void mkdir_p(const char *fmt, ...)
+void HTS_FORMAT(HTS_PRINTF_FMT, 1, 2)
+mkdir_p(const char *fmt, ...)
 {
     va_list ap;
     va_start(ap, fmt);
     int n = vsnprintf(NULL, 0, fmt, ap) + 2;
     va_end(ap);
 
-    char *path = (char*)malloc(n);
+    char *tmp = (char*)malloc(n);
+    if (!tmp) error("Couldn't allocate space for path: %s\n", strerror(errno));
     va_start(ap, fmt);
-    vsnprintf(path, n, fmt, ap);
+    vsnprintf(tmp, n, fmt, ap);
     va_end(ap);
 
-    char *tmp = strdup(path), *p = tmp+1;
+    char *p = tmp+1;
     while (*p)
     {
         while (*p && *p!='/') p++;
@@ -91,12 +94,11 @@ void mkdir_p(const char *fmt, ...)
         char ctmp = *p;
         *p = 0;
         int ret = mkdir(tmp,S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
-        if ( ret!=0 && errno!=EEXIST ) error("Error creating directory %s: %s\n", path,strerror(errno));
+        if ( ret!=0 && errno!=EEXIST ) error("Error creating directory %s: %s\n", tmp,strerror(errno));
         *p = ctmp;
         while ( *p && *p=='/' ) p++;
     }
     free(tmp);
-    free(path);
 }
 
 /**
@@ -107,7 +109,8 @@ void mkdir_p(const char *fmt, ...)
  *
  *  Returns open file descriptor or NULL if mode is NULL.
  */
-FILE *open_file(char **fname, const char *mode, const char *fmt, ...)
+FILE * HTS_FORMAT(HTS_PRINTF_FMT, 3, 4)
+open_file(char **fname, const char *mode, const char *fmt, ...)
 {
     va_list ap;
     va_start(ap, fmt);
@@ -119,7 +122,7 @@ FILE *open_file(char **fname, const char *mode, const char *fmt, ...)
     vsnprintf(str, n, fmt, ap);
     va_end(ap);
 
-    mkdir_p(str);
+    mkdir_p("%s", str);
     if ( !mode )
     {
         if ( !fname ) error("Uh: expected fname or mode\n");
@@ -152,8 +155,11 @@ void isec_vcf(args_t *args)
         if ( args->n_threads ) hts_set_threads(out_fh, args->n_threads);
         if (args->record_cmd_line) bcf_hdr_append_version(files->readers[args->iwrite].header,args->argc,args->argv,"bcftools_isec");
         if ( bcf_hdr_write(out_fh, files->readers[args->iwrite].header)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname?args->output_fname:"standard output");
-        if ( args->write_index && init_index(out_fh,files->readers[args->iwrite].header,args->output_fname,&args->index_fn)<0 )
-            error("Error: failed to initialise index for %s\n",args->output_fname?args->output_fname:"standard output");
+        if ( init_index2(out_fh,files->readers[args->iwrite].header,
+                         args->output_fname,&args->index_fn,
+                         args->write_index)<0 )
+            error("Error: failed to initialise index for %s\n",
+                  args->output_fname?args->output_fname:"standard output");
     }
     if ( !args->nwrite && !out_std && !args->prefix )
         fprintf(bcftools_stderr,"Note: -w option not given, printing list of sites...\n");
@@ -456,12 +462,14 @@ static void destroy_data(args_t *args)
         {
             if ( !args->fnames[i] ) continue;
             if ( hts_close(args->fh_out[i])!=0 ) error("[%s] Error: close failed .. %s\n", __func__,args->fnames[i]);
-            if ( args->output_type==FT_VCF_GZ )
+            int is_tbi = !args->write_index 
+                      || (args->write_index&127) == HTS_FMT_TBI;
+            if ( args->output_type==FT_VCF_GZ && is_tbi )
             {
                 tbx_conf_t conf = tbx_conf_vcf;
                 tbx_index_build(args->fnames[i], -1, &conf);
             }
-            else if ( args->output_type==FT_BCF_GZ )
+            else if ( args->output_type==FT_BCF_GZ || !is_tbi )
             {
                 if ( bcf_index_build(args->fnames[i],14) ) error("Could not index %s\n", args->fnames[i]);
             }
@@ -486,6 +494,7 @@ static void usage(void)
     fprintf(bcftools_stderr, "    -e, --exclude EXPR             Exclude sites for which the expression is true\n");
     fprintf(bcftools_stderr, "    -f, --apply-filters LIST       Require at least one of the listed FILTER strings (e.g. \"PASS,.\")\n");
     fprintf(bcftools_stderr, "    -i, --include EXPR             Include only sites for which the expression is true\n");
+    fprintf(bcftools_stderr, "    -l, --file-list FILE           Read the input file names from the file\n");
     fprintf(bcftools_stderr, "        --no-version               Do not append version and command line to the header\n");
     fprintf(bcftools_stderr, "    -n, --nfiles [+-=~]INT         Output positions present in this many (=), this many or more (+), this many or fewer (-), the exact (~) files\n");
     fprintf(bcftools_stderr, "    -o, --output FILE              Write output to a file [standard output]\n");
@@ -499,7 +508,7 @@ static void usage(void)
     fprintf(bcftools_stderr, "        --targets-overlap 0|1|2    Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n");
     fprintf(bcftools_stderr, "        --threads INT              Use multithreading with <int> worker threads [0]\n");
     fprintf(bcftools_stderr, "    -w, --write LIST               List of files to write with -p given as 1-based indexes. By default, all files are written\n");
-    fprintf(bcftools_stderr, "        --write-index              Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "    -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "Examples:\n");
     fprintf(bcftools_stderr, "   # Create intersection and complements of two sets saving the output in dir/*\n");
@@ -543,6 +552,7 @@ int main_vcfisec(int argc, char *argv[])
         {"collapse",required_argument,NULL,'c'},
         {"complement",no_argument,NULL,'C'},
         {"apply-filters",required_argument,NULL,'f'},
+        {"file-list",required_argument,NULL,'l'},
         {"nfiles",required_argument,NULL,'n'},
         {"prefix",required_argument,NULL,'p'},
         {"write",required_argument,NULL,'w'},
@@ -556,11 +566,11 @@ int main_vcfisec(int argc, char *argv[])
         {"output-type",required_argument,NULL,'O'},
         {"threads",required_argument,NULL,9},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,10},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "hc:r:R:p:n:w:t:T:Cf:o:O:i:e:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hc:r:R:p:n:w:t:T:Cf:o:O:i:e:l:W::",loptions,NULL)) >= 0) {
         switch (c) {
             case 'o': args->output_fname = optarg; break;
             case 'O':
@@ -595,12 +605,16 @@ int main_vcfisec(int argc, char *argv[])
             case 'C':
                 if ( args->isec_op!=0 && args->isec_op!=OP_COMPLEMENT ) error("Error: either -C or -n should be given, not both.\n");
                 args->isec_op = OP_COMPLEMENT; break;
+            case 'l': args->file_list = optarg; break;
             case 'r': args->regions_list = optarg; break;
             case 'R': args->regions_list = optarg; regions_is_file = 1; break;
             case 't': args->targets_list = optarg; break;
             case 'T': args->targets_list = optarg; targets_is_file = 1; break;
             case 'p': args->prefix = optarg; break;
-            case 'w': args->write_files = optarg; break;
+            case 'w':
+                if ( args->write_files ) error("The option -w accepts a list of indices and can be given only once\n");
+                args->write_files = optarg;
+                break;
             case 'i': add_filter(args, optarg, FLT_INCLUDE); break;
             case 'e': add_filter(args, optarg, FLT_EXCLUDE); break;
             case 'n':
@@ -628,13 +642,33 @@ int main_vcfisec(int argc, char *argv[])
                 break;
             case  9 : args->n_threads = strtol(optarg, 0, 0); break;
             case  8 : args->record_cmd_line = 0; break;
-            case 10 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'h':
             case '?': usage(); break;
             default: error("Unknown argument: %s\n", optarg);
         }
     }
-    if ( argc-optind<1 ) usage();   // no file given
+    if ( argc-optind<1 && !args->file_list ) usage();   // no file given
+
+    int nfiles = 0,i;
+    char **files = NULL;
+    if ( args->file_list )
+    {
+        files = hts_readlines(args->file_list, &nfiles);
+        if ( !files ) error("Failed to read from %s\n", args->file_list);
+    }
+    if ( optind<argc )
+    {
+        int n = argc - optind;
+        files = (char**)realloc(files,sizeof(*files)*(n+nfiles));
+        for (i=nfiles; i>0; i--) files[n+i-1] = files[n+i-2];
+        for (i=0; i<n; i++) files[i] = strdup(argv[optind+i]);
+        nfiles += n;
+    }
+
     if ( args->targets_list )
     {
         bcf_sr_set_opt(args->files,BCF_SR_TARGETS_OVERLAP,targets_overlap);
@@ -647,7 +681,7 @@ int main_vcfisec(int argc, char *argv[])
         if ( bcf_sr_set_regions(args->files, args->regions_list, regions_is_file)<0 )
             error("Failed to read the regions: %s\n", args->regions_list);
     }
-    if ( argc-optind==2 && !args->isec_op )
+    if ( nfiles==2 && !args->isec_op )
     {
         args->isec_op = OP_VENN;
         if ( !args->prefix ) error("Expected the -p option\n");
@@ -658,11 +692,13 @@ int main_vcfisec(int argc, char *argv[])
         args->isec_n  = 1;
     }
     args->files->require_index = 1;
-    while (optind<argc)
+    for (i=0; i<nfiles; i++)
     {
-        if ( !bcf_sr_add_reader(args->files, argv[optind]) ) error("Failed to open %s: %s\n", argv[optind],bcf_sr_strerror(args->files->errnum));
-        optind++;
+        if ( !bcf_sr_add_reader(args->files, files[i]) ) error("Failed to open %s: %s\n", files[i],bcf_sr_strerror(args->files->errnum));
+        free(files[i]);
     }
+    free(files);
+
     init_data(args);
     isec_vcf(args);
     destroy_data(args);
index 87b6b8a39d62eb7ec115044cfe851b2d47f11737..3ca5f287a49128e5340b0b67074b3151c478108f 100644 (file)
@@ -1,6 +1,6 @@
 /*  vcfmerge.c -- Merge multiple VCF/BCF files to create one multi-sample file.
 
-    Copyright (C) 2012-2023 Genome Research Ltd.
+    Copyright (C) 2012-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -34,6 +34,8 @@ THE SOFTWARE.  */
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
 #include <htslib/faidx.h>
+#include <htslib/kbitset.h>
+#include <htslib/hts_endian.h>
 #include <math.h>
 #include <ctype.h>
 #include <time.h>
@@ -172,7 +174,7 @@ typedef struct
     maux_t *maux;
     regidx_t *regs;    // apply regions only after the blocks are expanded
     regitr_t *regs_itr;
-    int header_only, collapse, output_type, force_samples, merge_by_id, do_gvcf, filter_logic, missing_to_ref, no_index;
+    int header_only, collapse, output_type, force_samples, force_single, merge_by_id, do_gvcf, filter_logic, missing_to_ref, no_index;
     char *header_fname, *output_fname, *regions_list, *info_rules, *file_list;
     faidx_t *gvcf_fai;
     info_rule_t *rules;
@@ -192,6 +194,7 @@ typedef struct
     int keep_AC_AN;
     char *index_fn;
     int write_index;
+    int trim_star_allele;   // 0=don't trim; 1=trim at variant sites; 2=trim at all sites
 }
 args_t;
 
@@ -437,6 +440,11 @@ static void info_rules_init(args_t *args)
             if ( str.l ) kputc(',',&str);
             kputs("QS:sum",&str);
         }
+        if ( args->do_gvcf && bcf_hdr_idinfo_exists(args->out_hdr,BCF_HL_INFO,bcf_hdr_id2int(args->out_hdr, BCF_DT_ID, "MIN_DP")) )
+        {
+            if ( str.l ) kputc(',',&str);
+            kputs("MIN_DP:min",&str);
+        }
         if ( args->do_gvcf && bcf_hdr_idinfo_exists(args->out_hdr,BCF_HL_INFO,bcf_hdr_id2int(args->out_hdr, BCF_DT_ID, "MinDP")) )
         {
             if ( str.l ) kputc(',',&str);
@@ -1272,32 +1280,32 @@ static void merge_AGR_info_tag(bcf_hdr_t *hdr, bcf1_t *line, bcf_info_t *info, i
         if ( len==BCF_VL_A || len==BCF_VL_R )
         {
             int ifrom = len==BCF_VL_A ? 1 : 0;
-            #define BRANCH(type_t, is_missing, is_vector_end, out_type_t) { \
-                type_t *src = (type_t *) info->vptr; \
+            #define BRANCH(type_t, convert, is_missing, is_vector_end, out_type_t) { \
+                uint8_t *src = info->vptr; \
                 out_type_t *tgt = (out_type_t *) agr->buf; \
                 int iori, inew; \
-                for (iori=ifrom; iori<line->n_allele; iori++) \
+                for (iori=ifrom; iori<line->n_allele; iori++, src += sizeof(type_t)) \
                 { \
+                    type_t val = convert(src); \
                     if ( is_vector_end ) break; \
                     if ( is_missing ) continue; \
                     inew = als->map[iori] - ifrom; \
-                    tgt[inew] = *src; \
-                    src++; \
+                    tgt[inew] = val; \
                 } \
             }
             switch (info->type) {
-                case BCF_BT_INT8:  BRANCH(int8_t,  *src==bcf_int8_missing,  *src==bcf_int8_vector_end,  int); break;
-                case BCF_BT_INT16: BRANCH(int16_t, *src==bcf_int16_missing, *src==bcf_int16_vector_end, int); break;
-                case BCF_BT_INT32: BRANCH(int32_t, *src==bcf_int32_missing, *src==bcf_int32_vector_end, int); break;
-                case BCF_BT_FLOAT: BRANCH(float,   bcf_float_is_missing(*src), bcf_float_is_vector_end(*src), float); break;
+                case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  int); break;
+                case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, int); break;
+                case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, int); break;
+                case BCF_BT_FLOAT: BRANCH(float,   le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), float); break;
                 default: fprintf(stderr,"TODO: %s:%d .. info->type=%d\n", __FILE__,__LINE__, info->type); exit(1);
             }
             #undef BRANCH
         }
         else
         {
-            #define BRANCH(type_t, is_missing, is_vector_end, out_type_t) { \
-                type_t *src = (type_t *) info->vptr; \
+            #define BRANCH(type_t, convert, is_missing, is_vector_end, out_type_t) { \
+                uint8_t *src = info->vptr; \
                 out_type_t *tgt = (out_type_t *) agr->buf; \
                 int iori,jori, inew,jnew; \
                 for (iori=0; iori<line->n_allele; iori++) \
@@ -1307,19 +1315,20 @@ static void merge_AGR_info_tag(bcf_hdr_t *hdr, bcf1_t *line, bcf_info_t *info, i
                     { \
                         jnew = als->map[jori]; \
                         int kori = iori*(iori+1)/2 + jori; \
+                        type_t val = convert(&src[kori * sizeof(type_t)]); \
                         if ( is_vector_end ) break; \
                         if ( is_missing ) continue; \
                         int knew = inew>jnew ? inew*(inew+1)/2 + jnew : jnew*(jnew+1)/2 + inew; \
-                        tgt[knew] = src[kori]; \
+                        tgt[knew] = val; \
                     } \
                     if ( jori<=iori ) break; \
                 } \
             }
             switch (info->type) {
-                case BCF_BT_INT8:  BRANCH(int8_t,  src[kori]==bcf_int8_missing,  src[kori]==bcf_int8_vector_end,  int); break;
-                case BCF_BT_INT16: BRANCH(int16_t, src[kori]==bcf_int16_missing, src[kori]==bcf_int16_vector_end, int); break;
-                case BCF_BT_INT32: BRANCH(int32_t, src[kori]==bcf_int32_missing, src[kori]==bcf_int32_vector_end, int); break;
-                case BCF_BT_FLOAT: BRANCH(float,   bcf_float_is_missing(src[kori]), bcf_float_is_vector_end(src[kori]), float); break;
+                case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  int); break;
+                case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, int); break;
+                case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, int); break;
+                case BCF_BT_FLOAT: BRANCH(float,   le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), float); break;
                 default: fprintf(stderr,"TODO: %s:%d .. info->type=%d\n", __FILE__,__LINE__, info->type); exit(1);
             }
             #undef BRANCH
@@ -1488,12 +1497,12 @@ static inline int max_used_gt_ploidy(bcf_fmt_t *fmt, int nsmpl)
 {
     int i,j, max_ploidy = 0;
 
-    #define BRANCH(type_t, vector_end) { \
-        type_t *ptr  = (type_t*) fmt->p; \
+    #define BRANCH(type_t, convert, vector_end) { \
+        uint8_t *ptr  = fmt->p; \
         for (i=0; i<nsmpl; i++) \
         { \
             for (j=0; j<fmt->n; j++) \
-                if ( ptr[j]==vector_end ) break; \
+                if ( convert(&ptr[j * sizeof(type_t)])==vector_end ) break; \
             if ( j==fmt->n ) \
             { \
                 /* all fields were used */ \
@@ -1501,14 +1510,14 @@ static inline int max_used_gt_ploidy(bcf_fmt_t *fmt, int nsmpl)
                 break; \
             } \
             if ( max_ploidy < j ) max_ploidy = j; \
-            ptr += fmt->n; \
+            ptr += fmt->n * sizeof(type_t); \
         } \
     }
     switch (fmt->type)
     {
-        case BCF_BT_INT8:  BRANCH(int8_t,   bcf_int8_vector_end); break;
-        case BCF_BT_INT16: BRANCH(int16_t, bcf_int16_vector_end); break;
-        case BCF_BT_INT32: BRANCH(int32_t, bcf_int32_vector_end); break;
+        case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  bcf_int8_vector_end); break;
+        case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, bcf_int16_vector_end); break;
+        case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, bcf_int32_vector_end); break;
         default: error("Unexpected case: %d\n", fmt->type);
     }
     #undef BRANCH
@@ -1598,19 +1607,22 @@ void init_local_alleles(args_t *args, bcf1_t *out, int ifmt_PL)
         int *map = ma->buf[i].rec[ma->buf[i].cur].map;
         double *allele_prob = ma->tmpd;
         int *idx = ma->tmpi;
-        #define BRANCH(src_type_t, src_is_missing, src_is_vector_end, pl2prob_idx) { \
-            src_type_t *src = (src_type_t*) fmt_ori->p; \
+        #define BRANCH(src_type_t, convert, src_is_missing, src_is_vector_end, pl2prob_idx) { \
+            uint8_t *src = fmt_ori->p; \
             for (j=0; j<nsmpl; j++) \
             { \
                 for (k=0; k<line->n_allele; k++) allele_prob[k] = 0; \
                 for (k=0; k<line->n_allele; k++) \
                     for (l=0; l<=k; l++) \
                     { \
-                        if ( src_is_missing || src_is_vector_end ) { src++; continue; } \
-                        double prob = ma->pl2prob[pl2prob_idx]; \
-                        allele_prob[k] += prob; \
-                        allele_prob[l] += prob; \
-                        src++; \
+                        src_type_t val = convert(src); \
+                        if ( !(src_is_missing) && !(src_is_vector_end) ) \
+                        { \
+                            double prob = ma->pl2prob[pl2prob_idx]; \
+                            allele_prob[k] += prob; \
+                            allele_prob[l] += prob; \
+                        } \
+                        src += sizeof(src_type_t); \
                     } \
                 /* insertion sort by allele probability, descending order, with the twist that REF (idx=0) always comes first */ \
                 allele_prob++; idx[0] = -1; idx++; /* keep REF first */ \
@@ -1637,9 +1649,9 @@ void init_local_alleles(args_t *args, bcf1_t *out, int ifmt_PL)
         }
         switch (fmt_ori->type)
         {
-            case BCF_BT_INT8:  BRANCH( int8_t, *src==bcf_int8_missing,  *src==bcf_int8_vector_end,  *src); break;
-            case BCF_BT_INT16: BRANCH(int16_t, *src==bcf_int16_missing, *src==bcf_int16_vector_end, *src>=0 && *src<PL2PROB_MAX ? *src : PL2PROB_MAX-1); break;
-            case BCF_BT_INT32: BRANCH(int32_t, *src==bcf_int32_missing, *src==bcf_int32_vector_end, *src>=0 && *src<PL2PROB_MAX ? *src : PL2PROB_MAX-1); break;
+            case BCF_BT_INT8:  BRANCH( int8_t, le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  val); break;
+            case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, val>=0 && val<PL2PROB_MAX ? val : PL2PROB_MAX-1); break;
+            case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, val>=0 && val<PL2PROB_MAX ? val : PL2PROB_MAX-1); break;
             default: error("Unexpected case: %d, PL\n", fmt_ori->type);
         }
         #undef BRANCH
@@ -1735,8 +1747,8 @@ void merge_GT(args_t *args, bcf_fmt_t **fmt_map, bcf1_t *out)
             continue;
         }
 
-        #define BRANCH(type_t, vector_end) { \
-            type_t *p_ori  = (type_t*) fmt_ori->p; \
+        #define BRANCH(type_t, convert, vector_end) { \
+            uint8_t *p_ori = fmt_ori->p; \
             if ( !ma->buf[i].rec[irec].als_differ ) \
             { \
                 /* the allele numbering is unchanged */ \
@@ -1744,14 +1756,15 @@ void merge_GT(args_t *args, bcf_fmt_t **fmt_map, bcf1_t *out)
                 { \
                     for (k=0; k<fmt_ori->n; k++) \
                     { \
-                        if ( p_ori[k]==vector_end ) break; /* smaller ploidy */ \
+                        type_t val = convert(&p_ori[k * sizeof(type_t)]); \
+                        if ( val==vector_end ) break; /* smaller ploidy */ \
                         ma->smpl_ploidy[ismpl+j]++; \
-                        if ( bcf_gt_is_missing(p_ori[k]) ) tmp[k] = 0; /* missing allele */ \
-                        else tmp[k] = p_ori[k]; \
+                        if ( bcf_gt_is_missing(val) ) tmp[k] = 0; /* missing allele */ \
+                        else tmp[k] = val; \
                     } \
                     for (; k<nsize; k++) tmp[k] = bcf_int32_vector_end; \
                     tmp += nsize; \
-                    p_ori += fmt_ori->n; \
+                    p_ori += fmt_ori->n * sizeof(type_t); \
                 } \
                 ismpl += bcf_hdr_nsamples(hdr); \
                 continue; \
@@ -1761,27 +1774,28 @@ void merge_GT(args_t *args, bcf_fmt_t **fmt_map, bcf1_t *out)
             { \
                 for (k=0; k<fmt_ori->n; k++) \
                 { \
-                    if ( p_ori[k]==vector_end ) break; /* smaller ploidy */ \
+                    type_t val = convert(&p_ori[k * sizeof(type_t)]); \
+                    if ( val==vector_end ) break; /* smaller ploidy */ \
                     ma->smpl_ploidy[ismpl+j]++; \
-                    if ( bcf_gt_is_missing(p_ori[k]) ) tmp[k] = 0; /* missing allele */ \
+                    if ( bcf_gt_is_missing(val) ) tmp[k] = 0; /* missing allele */ \
                     else \
                     { \
-                        int al = (p_ori[k]>>1) - 1; \
+                        int al = (val>>1) - 1; \
                         al = al<=0 ? al + 1 : ma->buf[i].rec[irec].map[al] + 1; \
-                        tmp[k] = (al << 1) | ((p_ori[k])&1); \
+                        tmp[k] = (al << 1) | ((val)&1); \
                     } \
                 } \
                 for (; k<nsize; k++) tmp[k] = bcf_int32_vector_end; \
                 tmp += nsize; \
-                p_ori += fmt_ori->n; \
+                p_ori += fmt_ori->n * sizeof(type_t); \
             } \
             ismpl += bcf_hdr_nsamples(hdr); \
         }
         switch (fmt_ori->type)
         {
-            case BCF_BT_INT8: BRANCH(int8_t,   bcf_int8_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int16_t, bcf_int16_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, bcf_int32_vector_end); break;
+            case BCF_BT_INT8: BRANCH(int8_t,   le_to_i8,  bcf_int8_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, bcf_int16_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, bcf_int32_vector_end); break;
             default: error("Unexpected case: %d\n", fmt_ori->type);
         }
         #undef BRANCH
@@ -1959,10 +1973,10 @@ void merge_localized_numberG_format_field(args_t *args, bcf_fmt_t **fmt_map, bcf
         if ( 2*fmt_ori->n!=line->n_allele*(line->n_allele+1) ) error("Todo: localization of missing or haploid Number=G tags\n");
 
         // localize
-        #define BRANCH(tgt_type_t, src_type_t, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
+        #define BRANCH(tgt_type_t, src_type_t, convert, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
             for (j=0; j<nsmpl; j++) \
             { \
-                src_type_t *src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
+                uint8_t *src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
                 tgt_type_t *tgt = (tgt_type_t *) ma->tmp_arr + ismpl*nsize; \
                 int *laa = ma->laa + (1+args->local_alleles)*ismpl; \
                 int ii,ij,tgt_idx = 0; \
@@ -1972,9 +1986,10 @@ void merge_localized_numberG_format_field(args_t *args, bcf_fmt_t **fmt_map, bcf
                     for (ij=0; ij<=ii; ij++) \
                     { \
                         int src_idx = bcf_alleles2gt(laa[ii],laa[ij]); \
+                        src_type_t val = convert(&src[src_idx * sizeof(src_type_t)]); \
                         if ( src_is_missing ) tgt_set_missing; \
                         else if ( src_is_vector_end ) break; \
-                        else tgt[tgt_idx] = src[src_idx]; \
+                        else tgt[tgt_idx] = val; \
                         tgt_idx++; \
                     } \
                 } \
@@ -1985,10 +2000,10 @@ void merge_localized_numberG_format_field(args_t *args, bcf_fmt_t **fmt_map, bcf
         }
         switch (fmt_ori->type)
         {
-            case BCF_BT_INT8:  BRANCH(int32_t,  int8_t, src[src_idx]==bcf_int8_missing,  src[src_idx]==bcf_int8_vector_end,  tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int32_t, int16_t, src[src_idx]==bcf_int16_missing, src[src_idx]==bcf_int16_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, int32_t, src[src_idx]==bcf_int32_missing, src[src_idx]==bcf_int32_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_FLOAT: BRANCH(float, float, bcf_float_is_missing(src[src_idx]), bcf_float_is_vector_end(src[src_idx]), bcf_float_set_missing(tgt[tgt_idx]), bcf_float_set_vector_end(tgt[tgt_idx])); break;
+            case BCF_BT_INT8:  BRANCH(int32_t, int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int32_t, int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, int32_t, le_to_i16, val==bcf_int32_missing, val==bcf_int32_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_FLOAT: BRANCH(float, float, le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), bcf_float_set_missing(tgt[tgt_idx]), bcf_float_set_vector_end(tgt[tgt_idx])); break;
             default: error("Unexpected case: %d, %s\n", fmt_ori->type, key);
         }
         #undef BRANCH
@@ -2058,10 +2073,10 @@ void merge_localized_numberAR_format_field(args_t *args, bcf_fmt_t **fmt_map, bc
         }
 
         // localize
-        #define BRANCH(tgt_type_t, src_type_t, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
+        #define BRANCH(tgt_type_t, src_type_t, convert, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
             for (j=0; j<nsmpl; j++) \
             { \
-                src_type_t *src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
+                uint8_t *src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
                 tgt_type_t *tgt = (tgt_type_t *) ma->tmp_arr + ismpl*nsize; \
                 int *laa = ma->laa + (1+args->local_alleles)*ismpl; \
                 int ii,tgt_idx = 0; \
@@ -2069,9 +2084,10 @@ void merge_localized_numberAR_format_field(args_t *args, bcf_fmt_t **fmt_map, bc
                 { \
                     if ( laa[ii]==bcf_int32_missing || laa[ii]==bcf_int32_vector_end ) break; \
                     int src_idx = laa[ii] - ibeg; \
+                    src_type_t val = convert(&src[src_idx * sizeof(src_type_t)]); \
                     if ( src_is_missing ) tgt_set_missing; \
                     else if ( src_is_vector_end ) break; \
-                    else tgt[tgt_idx] = src[src_idx]; \
+                    else tgt[tgt_idx] = val; \
                     tgt_idx++; \
                 } \
                 if ( !tgt_idx ) { tgt_set_missing; tgt_idx++; } \
@@ -2081,10 +2097,10 @@ void merge_localized_numberAR_format_field(args_t *args, bcf_fmt_t **fmt_map, bc
         }
         switch (fmt_ori->type)
         {
-            case BCF_BT_INT8:  BRANCH(int32_t,  int8_t, src[src_idx]==bcf_int8_missing,  src[src_idx]==bcf_int8_vector_end,  tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int32_t, int16_t, src[src_idx]==bcf_int16_missing, src[src_idx]==bcf_int16_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, int32_t, src[src_idx]==bcf_int32_missing, src[src_idx]==bcf_int32_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_FLOAT: BRANCH(float, float, bcf_float_is_missing(src[src_idx]), bcf_float_is_vector_end(src[src_idx]), bcf_float_set_missing(tgt[tgt_idx]), bcf_float_set_vector_end(tgt[tgt_idx])); break;
+            case BCF_BT_INT8:  BRANCH(int32_t, int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int32_t, int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_FLOAT: BRANCH(float, float, le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), bcf_float_set_missing(tgt[tgt_idx]), bcf_float_set_vector_end(tgt[tgt_idx])); break;
             default: error("Unexpected case: %d, %s\n", fmt_ori->type, key);
         }
         #undef BRANCH
@@ -2201,7 +2217,7 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
         }
 
         // set the values
-        #define BRANCH(tgt_type_t, src_type_t, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
+        #define BRANCH(tgt_type_t, src_type_t, convert, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
             int j, l, k; \
             tgt_type_t *tgt = (tgt_type_t *) ma->tmp_arr + ismpl*nsize; \
             if ( !fmt_ori ) \
@@ -2214,7 +2230,7 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                 ismpl += bcf_hdr_nsamples(hdr); \
                 continue; \
             } \
-            src_type_t *src = (src_type_t*) fmt_ori->p; \
+            uint8_t *src = fmt_ori->p; \
             if ( (length!=BCF_VL_G && length!=BCF_VL_A && length!=BCF_VL_R) || (line->n_allele==out->n_allele && !ma->buf[i].rec[irec].als_differ) ) \
             { \
                 /* alleles unchanged, copy over */ \
@@ -2224,11 +2240,11 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                     { \
                         if ( src_is_vector_end ) break; \
                         else if ( src_is_missing ) tgt_set_missing; \
-                        else *tgt = *src; \
-                        tgt++; src++; \
+                        else *tgt = convert(src); \
+                        tgt++; src += sizeof(src_type_t); \
                     } \
                     for (k=l; k<nsize; k++) { tgt_set_vector_end; tgt++; } \
-                    src += fmt_ori->n - l; \
+                    src += sizeof(src_type_t) * (fmt_ori->n - l); \
                 } \
                 ismpl += bcf_hdr_nsamples(hdr); \
                 continue; \
@@ -2240,8 +2256,13 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                 for (j=0; j<bcf_hdr_nsamples(hdr); j++) \
                 { \
                     tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize; \
-                    src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
-                    if ( (src_is_missing && fmt_ori->n==1) || (++src && src_is_vector_end) ) \
+                    src = fmt_ori->p + sizeof(src_type_t) * j * fmt_ori->n; \
+                    int tag_missing = src_is_missing && fmt_ori->n==1;  \
+                    if ( src_is_missing && fmt_ori->n>1 ) { \
+                        src += sizeof(src_type_t); \
+                        tag_missing = src_is_vector_end ; \
+                    } \
+                    if ( tag_missing ) \
                     { \
                         /* tag with missing value "." */ \
                         tgt_set_missing; \
@@ -2252,9 +2273,10 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                     int ngsize = haploid ? out->n_allele : out->n_allele*(out->n_allele + 1)/2; \
                     if ( ma->buf[i].unkn_allele )  /* Use value from the unknown allele when available */ \
                     {  \
-                        src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
+                        src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
                         int iunkn = haploid ? ma->buf[i].unkn_allele : (ma->buf[i].unkn_allele+1)*(ma->buf[i].unkn_allele + 2)/2 - 1; \
-                        for (l=0; l<ngsize; l++) { *tgt = src[iunkn]; tgt++; } \
+                        src_type_t val = convert(&src[iunkn * sizeof(src_type_t)]); \
+                        for (l=0; l<ngsize; l++) { *tgt = val; tgt++; } \
                     } \
                     else if ( mrule && mrule->type==MERGE_MISSING_CONST ) \
                     { \
@@ -2262,9 +2284,13 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                     } \
                     else if ( mrule && mrule->type==MERGE_MISSING_MAX ) \
                     { \
-                        src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
-                        src_type_t max = src[0]; \
-                        for (l=1; l<fmt_ori->n; l++) if ( max < src[l] ) max = src[l]; \
+                        src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
+                        src_type_t max = convert(src); \
+                        for (l=1; l<fmt_ori->n; l++) \
+                        { \
+                            src_type_t val = convert(&src[l * sizeof(src_type_t)]); \
+                            if ( max < val ) max = val; \
+                        } \
                         for (l=0; l<ngsize; l++) { *tgt = max; tgt++; } \
                     } \
                     else \
@@ -2278,11 +2304,11 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                         for (iori=0; iori<line->n_allele; iori++) \
                         { \
                             inew = ma->buf[i].rec[irec].map[iori]; \
-                            src = (src_type_t*) fmt_ori->p + j*fmt_ori->n + iori; \
+                            src = fmt_ori->p + (j*fmt_ori->n + iori) * sizeof(src_type_t); \
                             tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize + inew; \
                             if ( src_is_vector_end ) break; \
                             if ( src_is_missing ) tgt_set_missing; \
-                            else *tgt = *src; \
+                            else *tgt = convert(src); \
                         } \
                     } \
                     else \
@@ -2297,7 +2323,7 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                                 jnew = ma->buf[i].rec[irec].map[jori]; \
                                 int kori = iori*(iori+1)/2 + jori; \
                                 int knew = inew>jnew ? inew*(inew+1)/2 + jnew : jnew*(jnew+1)/2 + inew; \
-                                src = (src_type_t*) fmt_ori->p + j*fmt_ori->n + kori; \
+                                src = fmt_ori->p + (j*fmt_ori->n + kori) * sizeof(src_type_t); \
                                 tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize + knew; \
                                 if ( src_is_vector_end ) \
                                 { \
@@ -2305,7 +2331,7 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                                     break; \
                                 } \
                                 if ( src_is_missing ) tgt_set_missing; \
-                                else *tgt = *src; \
+                                else *tgt = convert(src); \
                             } \
                         } \
                     } \
@@ -2318,19 +2344,25 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                 for (j=0; j<bcf_hdr_nsamples(hdr); j++) \
                 { \
                     tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize; \
-                    src = (src_type_t*) (fmt_ori->p + j*fmt_ori->size); \
-                    if ( (src_is_missing && fmt_ori->n==1) || (++src && src_is_vector_end) ) \
+                    src = fmt_ori->p + j*fmt_ori->size; \
+                    int tag_missing = src_is_missing && fmt_ori->n==1;  \
+                    if ( src_is_missing && fmt_ori->n>1 ) { \
+                        src += sizeof(src_type_t); \
+                        tag_missing = src_is_vector_end ; \
+                    } \
+                    if ( tag_missing ) \
                     { \
                         /* tag with missing value "." */ \
                         tgt_set_missing; \
                         for (l=1; l<nsize; l++) { tgt++; tgt_set_vector_end; } \
                         continue; \
                     } \
-                    src = (src_type_t*) (fmt_ori->p + j*fmt_ori->size); \
+                    src = fmt_ori->p + j*fmt_ori->size; \
                     if ( ma->buf[i].unkn_allele )  /* Use value from the unknown allele when available */ \
                     { \
                         int iunkn = ma->buf[i].unkn_allele; \
-                        for (l=0; l<nsize; l++) { *tgt = src[iunkn]; tgt++; } \
+                        src_type_t val = convert(&src[iunkn * sizeof(src_type_t)]); \
+                        for (l=0; l<nsize; l++) { *tgt = val; tgt++; } \
                     } \
                     else if ( mrule && mrule->type==MERGE_MISSING_CONST ) \
                     { \
@@ -2338,9 +2370,13 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                     } \
                     else if ( mrule && mrule->type==MERGE_MISSING_MAX ) \
                     { \
-                        src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
-                        src_type_t max = src[0]; \
-                        for (l=1; l<fmt_ori->n; l++) if ( max < src[l] ) max = src[l]; \
+                        src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
+                        src_type_t max = convert(src); \
+                        for (l=1; l<fmt_ori->n; l++) \
+                        { \
+                            src_type_t val = convert(&src[l * sizeof(src_type_t)]); \
+                            if ( max < val ) max = val; \
+                        } \
                         for (l=0; l<nsize; l++) { *tgt = max; tgt++; } \
                     } \
                     else \
@@ -2354,8 +2390,8 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                         tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize + inew; \
                         if ( src_is_vector_end ) break; \
                         if ( src_is_missing ) tgt_set_missing; \
-                        else *tgt = *src; \
-                        src++; \
+                        else *tgt = convert(src); \
+                        src += sizeof(src_type_t); \
                     } \
                 } \
             } \
@@ -2363,10 +2399,10 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
         }
         switch (type)
         {
-            case BCF_BT_INT8:  BRANCH(int32_t,  int8_t, *src==bcf_int8_missing,  *src==bcf_int8_vector_end,  *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int32_t, int16_t, *src==bcf_int16_missing, *src==bcf_int16_vector_end, *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, int32_t, *src==bcf_int32_missing, *src==bcf_int32_vector_end, *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
-            case BCF_BT_FLOAT: BRANCH(float, float, bcf_float_is_missing(*src), bcf_float_is_vector_end(*src), bcf_float_set_missing(*tgt), bcf_float_set_vector_end(*tgt)); break;
+            case BCF_BT_INT8:  BRANCH(int32_t, int8_t,  le_to_i8,  le_to_i8(src)==bcf_int8_missing,  le_to_i8(src)==bcf_int8_vector_end,  *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int32_t, int16_t, le_to_i16, le_to_i16(src)==bcf_int16_missing, le_to_i16(src)==bcf_int16_vector_end, *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, int32_t, le_to_i32, le_to_i32(src)==bcf_int32_missing, le_to_i32(src)==bcf_int32_vector_end, *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
+            case BCF_BT_FLOAT: BRANCH(float, float, le_to_float, bcf_float_is_missing(le_to_float(src)), bcf_float_is_vector_end(le_to_float(src)), bcf_float_set_missing(*tgt), bcf_float_set_vector_end(*tgt)); break;
             default: error("Unexpected case: %d, %s\n", type, key);
         }
         #undef BRANCH
@@ -2582,10 +2618,20 @@ void gvcf_write_block(args_t *args, int start, int end)
     }
     else
         bcf_update_info_int32(args->out_hdr, out, "END", NULL, 0);
+
+    int iunseen;
+    if ( args->trim_star_allele && (out->n_allele > 2 || args->trim_star_allele > 1) && (iunseen=get_unseen_allele(out)) && iunseen>0 )
+    {
+        // the unobserved star allele should be trimmed, either it is variant site or trimming of all sites was requested
+        kbitset_t *rm_set = kbs_init(out->n_allele);
+        kbs_insert(rm_set, iunseen);
+        if ( bcf_remove_allele_set(args->out_hdr,out,rm_set) )
+            error("[%s] Error: failed to trim the unobserved allele at %s:%"PRIhts_pos"\n",__func__,bcf_seqname(args->out_hdr,out),out->pos+1);
+        kbs_destroy(rm_set);
+    }
     if ( bcf_write1(args->out_fh, args->out_hdr, out)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
     bcf_clear1(out);
 
-
     // Inactivate blocks which do not extend beyond END and find new gvcf_min
     min = INT_MAX;
     for (i=0; i<args->files->nreaders; i++)
@@ -3215,6 +3261,16 @@ void merge_line(args_t *args)
     if ( args->do_gvcf )
         bcf_update_info_int32(args->out_hdr, out, "END", NULL, 0);
     merge_format(args, out);
+    int iunseen;
+    if ( args->trim_star_allele && (out->n_allele > 2 || args->trim_star_allele > 1) && (iunseen=get_unseen_allele(out)) && iunseen>0 )
+    {
+        // the unobserved star allele should be trimmed, either it is variant site or trimming of all sites was requested
+        kbitset_t *rm_set = kbs_init(out->n_allele);
+        kbs_insert(rm_set, iunseen);
+        if ( bcf_remove_allele_set(args->out_hdr,out,rm_set) )
+            error("[%s] Error: failed to trim the unobserved allele at %s:%"PRIhts_pos"\n",__func__,bcf_seqname(args->out_hdr,out),out->pos+1);
+        kbs_destroy(rm_set);
+    }
     if ( bcf_write1(args->out_fh, args->out_hdr, out)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
     bcf_clear1(out);
 }
@@ -3344,9 +3400,11 @@ void merge_vcf(args_t *args)
         if ( hts_close(args->out_fh)!=0 ) error("[%s] Error: close failed .. %s\n", __func__,args->output_fname);
         return;
     }
-    else if ( args->write_index && init_index(args->out_fh,args->out_hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    else if ( init_index2(args->out_fh,args->out_hdr,args->output_fname,
+                          &args->index_fn, args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
 
-    if ( args->collapse==COLLAPSE_NONE ) args->vcmp = vcmp_init();
+    args->vcmp = vcmp_init();
     args->maux = maux_init(args);
     args->out_line = bcf_init1();
     args->tmph = kh_init(strdict);
@@ -3408,17 +3466,19 @@ static void usage(void)
     fprintf(stderr, "Usage:   bcftools merge [options] <A.vcf.gz> <B.vcf.gz> [...]\n");
     fprintf(stderr, "\n");
     fprintf(stderr, "Options:\n");
+    fprintf(stderr, "        --force-no-index              Merge unindexed files, synonymous to --no-index\n");
     fprintf(stderr, "        --force-samples               Resolve duplicate sample names\n");
+    fprintf(stderr, "        --force-single                Run even if there is only one file on input\n");
     fprintf(stderr, "        --print-header                Print only the merged header and exit\n");
     fprintf(stderr, "        --use-header FILE             Use the provided header\n");
     fprintf(stderr, "    -0  --missing-to-ref              Assume genotypes at missing sites are 0/0\n");
     fprintf(stderr, "    -f, --apply-filters LIST          Require at least one of the listed FILTER strings (e.g. \"PASS,.\")\n");
     fprintf(stderr, "    -F, --filter-logic x|+            Remove filters if some input is PASS (\"x\"), or apply all filters (\"+\") [+]\n");
-    fprintf(stderr, "    -g, --gvcf -|REF.FA               Merge gVCF blocks, INFO/END tag is expected. Implies -i QS:sum,MinDP:min,I16:sum,IDV:max,IMF:max -M PL:max,AD:0\n");
+    fprintf(stderr, "    -g, --gvcf -|REF.FA               Merge gVCF blocks, INFO/END tag is expected. Implies -i QS:sum,MinDP:min,MIN_DP:min,I16:sum,IDV:max,IMF:max -M PL:max,AD:0\n");
     fprintf(stderr, "    -i, --info-rules TAG:METHOD,..    Rules for merging INFO fields (method is one of sum,avg,min,max,join) or \"-\" to turn off the default [DP:sum,DP4:sum]\n");
     fprintf(stderr, "    -l, --file-list FILE              Read file names from the file\n");
-    fprintf(stderr, "    -L, --local-alleles INT           EXPERIMENTAL: if more than <int> ALT alleles are encountered, drop FMT/PL and output LAA+LPL instead; 0=unlimited [0]\n");
-    fprintf(stderr, "    -m, --merge STRING                Allow multiallelic records for <snps|indels|both|snp-ins-del|all|none|id>, see man page for details [both]\n");
+    fprintf(stderr, "    -L, --local-alleles INT           If more than INT alt alleles are encountered, drop FMT/PL and output LAA+LPL instead; 0=unlimited [0]\n");
+    fprintf(stderr, "    -m, --merge STRING[*|**]          Allow multiallelic records for snps,indels,both,snp-ins-del,all,none,id,*,**; see man page for details [both]\n");
     fprintf(stderr, "    -M, --missing-rules TAG:METHOD    Rules for replacing missing values in numeric vectors (.,0,max) when unknown allele <*> is not present [.]\n");
     fprintf(stderr, "        --no-index                    Merge unindexed files, the same chromosomal order is required and -r/-R are not allowed\n");
     fprintf(stderr, "        --no-version                  Do not append version and command line to the header\n");
@@ -3427,8 +3487,8 @@ static void usage(void)
     fprintf(stderr, "    -r, --regions REGION              Restrict to comma-separated list of regions\n");
     fprintf(stderr, "    -R, --regions-file FILE           Restrict to regions listed in a file\n");
     fprintf(stderr, "        --regions-overlap 0|1|2       Include if POS in the region (0), record overlaps (1), variant overlaps (2) [1]\n");
-    fprintf(stderr, "        --threads INT                 Use multithreading with <int> worker threads [0]\n");
-    fprintf(stderr, "        --write-index                 Automatically index the output files [off]\n");
+    fprintf(stderr, "        --threads INT                 Use multithreading with INT worker threads [0]\n");
+    fprintf(stderr, "    -W, --write-index[=FMT]           Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     exit(1);
 }
@@ -3470,12 +3530,14 @@ int main_vcfmerge(int argc, char *argv[])
         {"missing-rules",required_argument,NULL,'M'},
         {"no-version",no_argument,NULL,8},
         {"no-index",no_argument,NULL,10},
+        {"force-no-index",no_argument,NULL,10},
+        {"force-single",no_argument,NULL,12},
         {"filter-logic",required_argument,NULL,'F'},
-        {"write-index",no_argument,NULL,11},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "hm:f:r:R:o:O:i:M:l:g:F:0L:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hm:f:r:R:o:O:i:M:l:g:F:0L:W::",loptions,NULL)) >= 0) {
         switch (c) {
             case 'L':
                 args->local_alleles = strtol(optarg,&tmp,10);
@@ -3520,17 +3582,23 @@ int main_vcfmerge(int argc, char *argv[])
                 }
                 break;
             case 'm':
+            {
+                int len = strlen(optarg);
+                if ( optarg[len-1]=='*' ) { args->trim_star_allele++; len--; }
+                if ( optarg[len-1]=='*' ) { args->trim_star_allele++; len--; }
+                if ( optarg[len-1]==',' ) len--;
                 args->collapse = COLLAPSE_NONE;
-                if ( !strcmp(optarg,"snps") ) args->collapse |= COLLAPSE_SNPS;
-                else if ( !strcmp(optarg,"indels") ) args->collapse |= COLLAPSE_INDELS;
-                else if ( !strcmp(optarg,"both") ) args->collapse |= COLLAPSE_BOTH;
-                else if ( !strcmp(optarg,"any") ) args->collapse |= COLLAPSE_ANY;
-                else if ( !strcmp(optarg,"all") ) args->collapse |= COLLAPSE_ANY;
-                else if ( !strcmp(optarg,"none") ) args->collapse = COLLAPSE_NONE;
-                else if ( !strcmp(optarg,"snp-ins-del") ) args->collapse = COLLAPSE_SNP_INS_DEL|COLLAPSE_SNPS;
-                else if ( !strcmp(optarg,"id") ) { args->collapse = COLLAPSE_NONE; args->merge_by_id = 1; }
+                if ( !strncmp(optarg,"snp-ins-del",len) ) args->collapse = COLLAPSE_SNP_INS_DEL|COLLAPSE_SNPS;
+                else if ( !strncmp(optarg,"snps",len) ) args->collapse |= COLLAPSE_SNPS;
+                else if ( !strncmp(optarg,"indels",len) ) args->collapse |= COLLAPSE_INDELS;
+                else if ( !strncmp(optarg,"id",len) ) { args->collapse = COLLAPSE_NONE; args->merge_by_id = 1; }
+                else if ( !strncmp(optarg,"any",len) ) args->collapse |= COLLAPSE_ANY;
+                else if ( !strncmp(optarg,"all",len) ) args->collapse |= COLLAPSE_ANY;
+                else if ( !strncmp(optarg,"both",len) ) args->collapse |= COLLAPSE_BOTH;
+                else if ( !strncmp(optarg,"none",len) ) args->collapse = COLLAPSE_NONE;
                 else error("The -m type \"%s\" is not recognised.\n", optarg);
                 break;
+            }
             case 'f': args->files->apply_filters = optarg; break;
             case 'r': args->regions_list = optarg; break;
             case 'R': args->regions_list = optarg; regions_is_file = 1; break;
@@ -3544,15 +3612,17 @@ int main_vcfmerge(int argc, char *argv[])
             case  9 : args->n_threads = strtol(optarg, 0, 0); break;
             case  8 : args->record_cmd_line = 0; break;
             case 10 : args->no_index = 1; break;
-            case 11 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
+            case 12 : args->force_single = 1; break;
             case 'h':
             case '?': usage(); break;
             default: error("Unknown argument: %s\n", optarg);
         }
     }
     if ( argc==optind && !args->file_list ) usage();
-    if ( argc-optind<2 && !args->file_list ) usage();
-
     if ( args->no_index )
     {
         if ( args->regions_list ) error("Error: cannot combine --no-index with -r/-R\n");
@@ -3593,6 +3663,9 @@ int main_vcfmerge(int argc, char *argv[])
         for (i=0; i<nfiles; i++) free(files[i]);
         free(files);
     }
+    if ( !args->files->nreaders ) usage();
+    if ( args->files->nreaders==1 && !args->force_single ) error("Expected two or more files to merge, got only one. Use --force-single to proceed anyway\n");
+
     merge_vcf(args);
     bcf_sr_destroy(args->files);
     if ( args->regs ) regidx_destroy(args->regs);
index 7ce5dfa8d1e36bf609844014323dfddb108e0207..d0802d07a0926783dbe3eaac2a5caf1e8ccd482f 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  vcfmerge.c -- Merge multiple VCF/BCF files to create one multi-sample file.
 
-    Copyright (C) 2012-2023 Genome Research Ltd.
+    Copyright (C) 2012-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -36,6 +36,8 @@ THE SOFTWARE.  */
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
 #include <htslib/faidx.h>
+#include <htslib/kbitset.h>
+#include <htslib/hts_endian.h>
 #include <math.h>
 #include <ctype.h>
 #include <time.h>
@@ -174,7 +176,7 @@ typedef struct
     maux_t *maux;
     regidx_t *regs;    // apply regions only after the blocks are expanded
     regitr_t *regs_itr;
-    int header_only, collapse, output_type, force_samples, merge_by_id, do_gvcf, filter_logic, missing_to_ref, no_index;
+    int header_only, collapse, output_type, force_samples, force_single, merge_by_id, do_gvcf, filter_logic, missing_to_ref, no_index;
     char *header_fname, *output_fname, *regions_list, *info_rules, *file_list;
     faidx_t *gvcf_fai;
     info_rule_t *rules;
@@ -194,6 +196,7 @@ typedef struct
     int keep_AC_AN;
     char *index_fn;
     int write_index;
+    int trim_star_allele;   // 0=don't trim; 1=trim at variant sites; 2=trim at all sites
 }
 args_t;
 
@@ -439,6 +442,11 @@ static void info_rules_init(args_t *args)
             if ( str.l ) kputc(',',&str);
             kputs("QS:sum",&str);
         }
+        if ( args->do_gvcf && bcf_hdr_idinfo_exists(args->out_hdr,BCF_HL_INFO,bcf_hdr_id2int(args->out_hdr, BCF_DT_ID, "MIN_DP")) )
+        {
+            if ( str.l ) kputc(',',&str);
+            kputs("MIN_DP:min",&str);
+        }
         if ( args->do_gvcf && bcf_hdr_idinfo_exists(args->out_hdr,BCF_HL_INFO,bcf_hdr_id2int(args->out_hdr, BCF_DT_ID, "MinDP")) )
         {
             if ( str.l ) kputc(',',&str);
@@ -1274,32 +1282,32 @@ static void merge_AGR_info_tag(bcf_hdr_t *hdr, bcf1_t *line, bcf_info_t *info, i
         if ( len==BCF_VL_A || len==BCF_VL_R )
         {
             int ifrom = len==BCF_VL_A ? 1 : 0;
-            #define BRANCH(type_t, is_missing, is_vector_end, out_type_t) { \
-                type_t *src = (type_t *) info->vptr; \
+            #define BRANCH(type_t, convert, is_missing, is_vector_end, out_type_t) { \
+                uint8_t *src = info->vptr; \
                 out_type_t *tgt = (out_type_t *) agr->buf; \
                 int iori, inew; \
-                for (iori=ifrom; iori<line->n_allele; iori++) \
+                for (iori=ifrom; iori<line->n_allele; iori++, src += sizeof(type_t)) \
                 { \
+                    type_t val = convert(src); \
                     if ( is_vector_end ) break; \
                     if ( is_missing ) continue; \
                     inew = als->map[iori] - ifrom; \
-                    tgt[inew] = *src; \
-                    src++; \
+                    tgt[inew] = val; \
                 } \
             }
             switch (info->type) {
-                case BCF_BT_INT8:  BRANCH(int8_t,  *src==bcf_int8_missing,  *src==bcf_int8_vector_end,  int); break;
-                case BCF_BT_INT16: BRANCH(int16_t, *src==bcf_int16_missing, *src==bcf_int16_vector_end, int); break;
-                case BCF_BT_INT32: BRANCH(int32_t, *src==bcf_int32_missing, *src==bcf_int32_vector_end, int); break;
-                case BCF_BT_FLOAT: BRANCH(float,   bcf_float_is_missing(*src), bcf_float_is_vector_end(*src), float); break;
+                case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  int); break;
+                case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, int); break;
+                case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, int); break;
+                case BCF_BT_FLOAT: BRANCH(float,   le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), float); break;
                 default: fprintf(bcftools_stderr,"TODO: %s:%d .. info->type=%d\n", __FILE__,__LINE__, info->type); bcftools_exit(1);
             }
             #undef BRANCH
         }
         else
         {
-            #define BRANCH(type_t, is_missing, is_vector_end, out_type_t) { \
-                type_t *src = (type_t *) info->vptr; \
+            #define BRANCH(type_t, convert, is_missing, is_vector_end, out_type_t) { \
+                uint8_t *src = info->vptr; \
                 out_type_t *tgt = (out_type_t *) agr->buf; \
                 int iori,jori, inew,jnew; \
                 for (iori=0; iori<line->n_allele; iori++) \
@@ -1309,19 +1317,20 @@ static void merge_AGR_info_tag(bcf_hdr_t *hdr, bcf1_t *line, bcf_info_t *info, i
                     { \
                         jnew = als->map[jori]; \
                         int kori = iori*(iori+1)/2 + jori; \
+                        type_t val = convert(&src[kori * sizeof(type_t)]); \
                         if ( is_vector_end ) break; \
                         if ( is_missing ) continue; \
                         int knew = inew>jnew ? inew*(inew+1)/2 + jnew : jnew*(jnew+1)/2 + inew; \
-                        tgt[knew] = src[kori]; \
+                        tgt[knew] = val; \
                     } \
                     if ( jori<=iori ) break; \
                 } \
             }
             switch (info->type) {
-                case BCF_BT_INT8:  BRANCH(int8_t,  src[kori]==bcf_int8_missing,  src[kori]==bcf_int8_vector_end,  int); break;
-                case BCF_BT_INT16: BRANCH(int16_t, src[kori]==bcf_int16_missing, src[kori]==bcf_int16_vector_end, int); break;
-                case BCF_BT_INT32: BRANCH(int32_t, src[kori]==bcf_int32_missing, src[kori]==bcf_int32_vector_end, int); break;
-                case BCF_BT_FLOAT: BRANCH(float,   bcf_float_is_missing(src[kori]), bcf_float_is_vector_end(src[kori]), float); break;
+                case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  int); break;
+                case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, int); break;
+                case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, int); break;
+                case BCF_BT_FLOAT: BRANCH(float,   le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), float); break;
                 default: fprintf(bcftools_stderr,"TODO: %s:%d .. info->type=%d\n", __FILE__,__LINE__, info->type); bcftools_exit(1);
             }
             #undef BRANCH
@@ -1490,12 +1499,12 @@ static inline int max_used_gt_ploidy(bcf_fmt_t *fmt, int nsmpl)
 {
     int i,j, max_ploidy = 0;
 
-    #define BRANCH(type_t, vector_end) { \
-        type_t *ptr  = (type_t*) fmt->p; \
+    #define BRANCH(type_t, convert, vector_end) { \
+        uint8_t *ptr  = fmt->p; \
         for (i=0; i<nsmpl; i++) \
         { \
             for (j=0; j<fmt->n; j++) \
-                if ( ptr[j]==vector_end ) break; \
+                if ( convert(&ptr[j * sizeof(type_t)])==vector_end ) break; \
             if ( j==fmt->n ) \
             { \
                 /* all fields were used */ \
@@ -1503,14 +1512,14 @@ static inline int max_used_gt_ploidy(bcf_fmt_t *fmt, int nsmpl)
                 break; \
             } \
             if ( max_ploidy < j ) max_ploidy = j; \
-            ptr += fmt->n; \
+            ptr += fmt->n * sizeof(type_t); \
         } \
     }
     switch (fmt->type)
     {
-        case BCF_BT_INT8:  BRANCH(int8_t,   bcf_int8_vector_end); break;
-        case BCF_BT_INT16: BRANCH(int16_t, bcf_int16_vector_end); break;
-        case BCF_BT_INT32: BRANCH(int32_t, bcf_int32_vector_end); break;
+        case BCF_BT_INT8:  BRANCH(int8_t,  le_to_i8,  bcf_int8_vector_end); break;
+        case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, bcf_int16_vector_end); break;
+        case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, bcf_int32_vector_end); break;
         default: error("Unexpected case: %d\n", fmt->type);
     }
     #undef BRANCH
@@ -1600,19 +1609,22 @@ void init_local_alleles(args_t *args, bcf1_t *out, int ifmt_PL)
         int *map = ma->buf[i].rec[ma->buf[i].cur].map;
         double *allele_prob = ma->tmpd;
         int *idx = ma->tmpi;
-        #define BRANCH(src_type_t, src_is_missing, src_is_vector_end, pl2prob_idx) { \
-            src_type_t *src = (src_type_t*) fmt_ori->p; \
+        #define BRANCH(src_type_t, convert, src_is_missing, src_is_vector_end, pl2prob_idx) { \
+            uint8_t *src = fmt_ori->p; \
             for (j=0; j<nsmpl; j++) \
             { \
                 for (k=0; k<line->n_allele; k++) allele_prob[k] = 0; \
                 for (k=0; k<line->n_allele; k++) \
                     for (l=0; l<=k; l++) \
                     { \
-                        if ( src_is_missing || src_is_vector_end ) { src++; continue; } \
-                        double prob = ma->pl2prob[pl2prob_idx]; \
-                        allele_prob[k] += prob; \
-                        allele_prob[l] += prob; \
-                        src++; \
+                        src_type_t val = convert(src); \
+                        if ( !(src_is_missing) && !(src_is_vector_end) ) \
+                        { \
+                            double prob = ma->pl2prob[pl2prob_idx]; \
+                            allele_prob[k] += prob; \
+                            allele_prob[l] += prob; \
+                        } \
+                        src += sizeof(src_type_t); \
                     } \
                 /* insertion sort by allele probability, descending order, with the twist that REF (idx=0) always comes first */ \
                 allele_prob++; idx[0] = -1; idx++; /* keep REF first */ \
@@ -1639,9 +1651,9 @@ void init_local_alleles(args_t *args, bcf1_t *out, int ifmt_PL)
         }
         switch (fmt_ori->type)
         {
-            case BCF_BT_INT8:  BRANCH( int8_t, *src==bcf_int8_missing,  *src==bcf_int8_vector_end,  *src); break;
-            case BCF_BT_INT16: BRANCH(int16_t, *src==bcf_int16_missing, *src==bcf_int16_vector_end, *src>=0 && *src<PL2PROB_MAX ? *src : PL2PROB_MAX-1); break;
-            case BCF_BT_INT32: BRANCH(int32_t, *src==bcf_int32_missing, *src==bcf_int32_vector_end, *src>=0 && *src<PL2PROB_MAX ? *src : PL2PROB_MAX-1); break;
+            case BCF_BT_INT8:  BRANCH( int8_t, le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  val); break;
+            case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, val>=0 && val<PL2PROB_MAX ? val : PL2PROB_MAX-1); break;
+            case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, val>=0 && val<PL2PROB_MAX ? val : PL2PROB_MAX-1); break;
             default: error("Unexpected case: %d, PL\n", fmt_ori->type);
         }
         #undef BRANCH
@@ -1737,8 +1749,8 @@ void merge_GT(args_t *args, bcf_fmt_t **fmt_map, bcf1_t *out)
             continue;
         }
 
-        #define BRANCH(type_t, vector_end) { \
-            type_t *p_ori  = (type_t*) fmt_ori->p; \
+        #define BRANCH(type_t, convert, vector_end) { \
+            uint8_t *p_ori = fmt_ori->p; \
             if ( !ma->buf[i].rec[irec].als_differ ) \
             { \
                 /* the allele numbering is unchanged */ \
@@ -1746,14 +1758,15 @@ void merge_GT(args_t *args, bcf_fmt_t **fmt_map, bcf1_t *out)
                 { \
                     for (k=0; k<fmt_ori->n; k++) \
                     { \
-                        if ( p_ori[k]==vector_end ) break; /* smaller ploidy */ \
+                        type_t val = convert(&p_ori[k * sizeof(type_t)]); \
+                        if ( val==vector_end ) break; /* smaller ploidy */ \
                         ma->smpl_ploidy[ismpl+j]++; \
-                        if ( bcf_gt_is_missing(p_ori[k]) ) tmp[k] = 0; /* missing allele */ \
-                        else tmp[k] = p_ori[k]; \
+                        if ( bcf_gt_is_missing(val) ) tmp[k] = 0; /* missing allele */ \
+                        else tmp[k] = val; \
                     } \
                     for (; k<nsize; k++) tmp[k] = bcf_int32_vector_end; \
                     tmp += nsize; \
-                    p_ori += fmt_ori->n; \
+                    p_ori += fmt_ori->n * sizeof(type_t); \
                 } \
                 ismpl += bcf_hdr_nsamples(hdr); \
                 continue; \
@@ -1763,27 +1776,28 @@ void merge_GT(args_t *args, bcf_fmt_t **fmt_map, bcf1_t *out)
             { \
                 for (k=0; k<fmt_ori->n; k++) \
                 { \
-                    if ( p_ori[k]==vector_end ) break; /* smaller ploidy */ \
+                    type_t val = convert(&p_ori[k * sizeof(type_t)]); \
+                    if ( val==vector_end ) break; /* smaller ploidy */ \
                     ma->smpl_ploidy[ismpl+j]++; \
-                    if ( bcf_gt_is_missing(p_ori[k]) ) tmp[k] = 0; /* missing allele */ \
+                    if ( bcf_gt_is_missing(val) ) tmp[k] = 0; /* missing allele */ \
                     else \
                     { \
-                        int al = (p_ori[k]>>1) - 1; \
+                        int al = (val>>1) - 1; \
                         al = al<=0 ? al + 1 : ma->buf[i].rec[irec].map[al] + 1; \
-                        tmp[k] = (al << 1) | ((p_ori[k])&1); \
+                        tmp[k] = (al << 1) | ((val)&1); \
                     } \
                 } \
                 for (; k<nsize; k++) tmp[k] = bcf_int32_vector_end; \
                 tmp += nsize; \
-                p_ori += fmt_ori->n; \
+                p_ori += fmt_ori->n * sizeof(type_t); \
             } \
             ismpl += bcf_hdr_nsamples(hdr); \
         }
         switch (fmt_ori->type)
         {
-            case BCF_BT_INT8: BRANCH(int8_t,   bcf_int8_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int16_t, bcf_int16_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, bcf_int32_vector_end); break;
+            case BCF_BT_INT8: BRANCH(int8_t,   le_to_i8,  bcf_int8_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int16_t, le_to_i16, bcf_int16_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, le_to_i32, bcf_int32_vector_end); break;
             default: error("Unexpected case: %d\n", fmt_ori->type);
         }
         #undef BRANCH
@@ -1961,10 +1975,10 @@ void merge_localized_numberG_format_field(args_t *args, bcf_fmt_t **fmt_map, bcf
         if ( 2*fmt_ori->n!=line->n_allele*(line->n_allele+1) ) error("Todo: localization of missing or haploid Number=G tags\n");
 
         // localize
-        #define BRANCH(tgt_type_t, src_type_t, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
+        #define BRANCH(tgt_type_t, src_type_t, convert, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
             for (j=0; j<nsmpl; j++) \
             { \
-                src_type_t *src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
+                uint8_t *src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
                 tgt_type_t *tgt = (tgt_type_t *) ma->tmp_arr + ismpl*nsize; \
                 int *laa = ma->laa + (1+args->local_alleles)*ismpl; \
                 int ii,ij,tgt_idx = 0; \
@@ -1974,9 +1988,10 @@ void merge_localized_numberG_format_field(args_t *args, bcf_fmt_t **fmt_map, bcf
                     for (ij=0; ij<=ii; ij++) \
                     { \
                         int src_idx = bcf_alleles2gt(laa[ii],laa[ij]); \
+                        src_type_t val = convert(&src[src_idx * sizeof(src_type_t)]); \
                         if ( src_is_missing ) tgt_set_missing; \
                         else if ( src_is_vector_end ) break; \
-                        else tgt[tgt_idx] = src[src_idx]; \
+                        else tgt[tgt_idx] = val; \
                         tgt_idx++; \
                     } \
                 } \
@@ -1987,10 +2002,10 @@ void merge_localized_numberG_format_field(args_t *args, bcf_fmt_t **fmt_map, bcf
         }
         switch (fmt_ori->type)
         {
-            case BCF_BT_INT8:  BRANCH(int32_t,  int8_t, src[src_idx]==bcf_int8_missing,  src[src_idx]==bcf_int8_vector_end,  tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int32_t, int16_t, src[src_idx]==bcf_int16_missing, src[src_idx]==bcf_int16_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, int32_t, src[src_idx]==bcf_int32_missing, src[src_idx]==bcf_int32_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_FLOAT: BRANCH(float, float, bcf_float_is_missing(src[src_idx]), bcf_float_is_vector_end(src[src_idx]), bcf_float_set_missing(tgt[tgt_idx]), bcf_float_set_vector_end(tgt[tgt_idx])); break;
+            case BCF_BT_INT8:  BRANCH(int32_t, int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int32_t, int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, int32_t, le_to_i16, val==bcf_int32_missing, val==bcf_int32_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_FLOAT: BRANCH(float, float, le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), bcf_float_set_missing(tgt[tgt_idx]), bcf_float_set_vector_end(tgt[tgt_idx])); break;
             default: error("Unexpected case: %d, %s\n", fmt_ori->type, key);
         }
         #undef BRANCH
@@ -2060,10 +2075,10 @@ void merge_localized_numberAR_format_field(args_t *args, bcf_fmt_t **fmt_map, bc
         }
 
         // localize
-        #define BRANCH(tgt_type_t, src_type_t, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
+        #define BRANCH(tgt_type_t, src_type_t, convert, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
             for (j=0; j<nsmpl; j++) \
             { \
-                src_type_t *src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
+                uint8_t *src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
                 tgt_type_t *tgt = (tgt_type_t *) ma->tmp_arr + ismpl*nsize; \
                 int *laa = ma->laa + (1+args->local_alleles)*ismpl; \
                 int ii,tgt_idx = 0; \
@@ -2071,9 +2086,10 @@ void merge_localized_numberAR_format_field(args_t *args, bcf_fmt_t **fmt_map, bc
                 { \
                     if ( laa[ii]==bcf_int32_missing || laa[ii]==bcf_int32_vector_end ) break; \
                     int src_idx = laa[ii] - ibeg; \
+                    src_type_t val = convert(&src[src_idx * sizeof(src_type_t)]); \
                     if ( src_is_missing ) tgt_set_missing; \
                     else if ( src_is_vector_end ) break; \
-                    else tgt[tgt_idx] = src[src_idx]; \
+                    else tgt[tgt_idx] = val; \
                     tgt_idx++; \
                 } \
                 if ( !tgt_idx ) { tgt_set_missing; tgt_idx++; } \
@@ -2083,10 +2099,10 @@ void merge_localized_numberAR_format_field(args_t *args, bcf_fmt_t **fmt_map, bc
         }
         switch (fmt_ori->type)
         {
-            case BCF_BT_INT8:  BRANCH(int32_t,  int8_t, src[src_idx]==bcf_int8_missing,  src[src_idx]==bcf_int8_vector_end,  tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int32_t, int16_t, src[src_idx]==bcf_int16_missing, src[src_idx]==bcf_int16_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, int32_t, src[src_idx]==bcf_int32_missing, src[src_idx]==bcf_int32_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
-            case BCF_BT_FLOAT: BRANCH(float, float, bcf_float_is_missing(src[src_idx]), bcf_float_is_vector_end(src[src_idx]), bcf_float_set_missing(tgt[tgt_idx]), bcf_float_set_vector_end(tgt[tgt_idx])); break;
+            case BCF_BT_INT8:  BRANCH(int32_t, int8_t,  le_to_i8,  val==bcf_int8_missing,  val==bcf_int8_vector_end,  tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int32_t, int16_t, le_to_i16, val==bcf_int16_missing, val==bcf_int16_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, int32_t, le_to_i32, val==bcf_int32_missing, val==bcf_int32_vector_end, tgt[tgt_idx]=bcf_int32_missing, tgt[tgt_idx]=bcf_int32_vector_end); break;
+            case BCF_BT_FLOAT: BRANCH(float, float, le_to_float, bcf_float_is_missing(val), bcf_float_is_vector_end(val), bcf_float_set_missing(tgt[tgt_idx]), bcf_float_set_vector_end(tgt[tgt_idx])); break;
             default: error("Unexpected case: %d, %s\n", fmt_ori->type, key);
         }
         #undef BRANCH
@@ -2203,7 +2219,7 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
         }
 
         // set the values
-        #define BRANCH(tgt_type_t, src_type_t, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
+        #define BRANCH(tgt_type_t, src_type_t, convert, src_is_missing, src_is_vector_end, tgt_set_missing, tgt_set_vector_end) { \
             int j, l, k; \
             tgt_type_t *tgt = (tgt_type_t *) ma->tmp_arr + ismpl*nsize; \
             if ( !fmt_ori ) \
@@ -2216,7 +2232,7 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                 ismpl += bcf_hdr_nsamples(hdr); \
                 continue; \
             } \
-            src_type_t *src = (src_type_t*) fmt_ori->p; \
+            uint8_t *src = fmt_ori->p; \
             if ( (length!=BCF_VL_G && length!=BCF_VL_A && length!=BCF_VL_R) || (line->n_allele==out->n_allele && !ma->buf[i].rec[irec].als_differ) ) \
             { \
                 /* alleles unchanged, copy over */ \
@@ -2226,11 +2242,11 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                     { \
                         if ( src_is_vector_end ) break; \
                         else if ( src_is_missing ) tgt_set_missing; \
-                        else *tgt = *src; \
-                        tgt++; src++; \
+                        else *tgt = convert(src); \
+                        tgt++; src += sizeof(src_type_t); \
                     } \
                     for (k=l; k<nsize; k++) { tgt_set_vector_end; tgt++; } \
-                    src += fmt_ori->n - l; \
+                    src += sizeof(src_type_t) * (fmt_ori->n - l); \
                 } \
                 ismpl += bcf_hdr_nsamples(hdr); \
                 continue; \
@@ -2242,8 +2258,13 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                 for (j=0; j<bcf_hdr_nsamples(hdr); j++) \
                 { \
                     tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize; \
-                    src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
-                    if ( (src_is_missing && fmt_ori->n==1) || (++src && src_is_vector_end) ) \
+                    src = fmt_ori->p + sizeof(src_type_t) * j * fmt_ori->n; \
+                    int tag_missing = src_is_missing && fmt_ori->n==1;  \
+                    if ( src_is_missing && fmt_ori->n>1 ) { \
+                        src += sizeof(src_type_t); \
+                        tag_missing = src_is_vector_end ; \
+                    } \
+                    if ( tag_missing ) \
                     { \
                         /* tag with missing value "." */ \
                         tgt_set_missing; \
@@ -2254,9 +2275,10 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                     int ngsize = haploid ? out->n_allele : out->n_allele*(out->n_allele + 1)/2; \
                     if ( ma->buf[i].unkn_allele )  /* Use value from the unknown allele when available */ \
                     {  \
-                        src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
+                        src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
                         int iunkn = haploid ? ma->buf[i].unkn_allele : (ma->buf[i].unkn_allele+1)*(ma->buf[i].unkn_allele + 2)/2 - 1; \
-                        for (l=0; l<ngsize; l++) { *tgt = src[iunkn]; tgt++; } \
+                        src_type_t val = convert(&src[iunkn * sizeof(src_type_t)]); \
+                        for (l=0; l<ngsize; l++) { *tgt = val; tgt++; } \
                     } \
                     else if ( mrule && mrule->type==MERGE_MISSING_CONST ) \
                     { \
@@ -2264,9 +2286,13 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                     } \
                     else if ( mrule && mrule->type==MERGE_MISSING_MAX ) \
                     { \
-                        src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
-                        src_type_t max = src[0]; \
-                        for (l=1; l<fmt_ori->n; l++) if ( max < src[l] ) max = src[l]; \
+                        src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
+                        src_type_t max = convert(src); \
+                        for (l=1; l<fmt_ori->n; l++) \
+                        { \
+                            src_type_t val = convert(&src[l * sizeof(src_type_t)]); \
+                            if ( max < val ) max = val; \
+                        } \
                         for (l=0; l<ngsize; l++) { *tgt = max; tgt++; } \
                     } \
                     else \
@@ -2280,11 +2306,11 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                         for (iori=0; iori<line->n_allele; iori++) \
                         { \
                             inew = ma->buf[i].rec[irec].map[iori]; \
-                            src = (src_type_t*) fmt_ori->p + j*fmt_ori->n + iori; \
+                            src = fmt_ori->p + (j*fmt_ori->n + iori) * sizeof(src_type_t); \
                             tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize + inew; \
                             if ( src_is_vector_end ) break; \
                             if ( src_is_missing ) tgt_set_missing; \
-                            else *tgt = *src; \
+                            else *tgt = convert(src); \
                         } \
                     } \
                     else \
@@ -2299,7 +2325,7 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                                 jnew = ma->buf[i].rec[irec].map[jori]; \
                                 int kori = iori*(iori+1)/2 + jori; \
                                 int knew = inew>jnew ? inew*(inew+1)/2 + jnew : jnew*(jnew+1)/2 + inew; \
-                                src = (src_type_t*) fmt_ori->p + j*fmt_ori->n + kori; \
+                                src = fmt_ori->p + (j*fmt_ori->n + kori) * sizeof(src_type_t); \
                                 tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize + knew; \
                                 if ( src_is_vector_end ) \
                                 { \
@@ -2307,7 +2333,7 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                                     break; \
                                 } \
                                 if ( src_is_missing ) tgt_set_missing; \
-                                else *tgt = *src; \
+                                else *tgt = convert(src); \
                             } \
                         } \
                     } \
@@ -2320,19 +2346,25 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                 for (j=0; j<bcf_hdr_nsamples(hdr); j++) \
                 { \
                     tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize; \
-                    src = (src_type_t*) (fmt_ori->p + j*fmt_ori->size); \
-                    if ( (src_is_missing && fmt_ori->n==1) || (++src && src_is_vector_end) ) \
+                    src = fmt_ori->p + j*fmt_ori->size; \
+                    int tag_missing = src_is_missing && fmt_ori->n==1;  \
+                    if ( src_is_missing && fmt_ori->n>1 ) { \
+                        src += sizeof(src_type_t); \
+                        tag_missing = src_is_vector_end ; \
+                    } \
+                    if ( tag_missing ) \
                     { \
                         /* tag with missing value "." */ \
                         tgt_set_missing; \
                         for (l=1; l<nsize; l++) { tgt++; tgt_set_vector_end; } \
                         continue; \
                     } \
-                    src = (src_type_t*) (fmt_ori->p + j*fmt_ori->size); \
+                    src = fmt_ori->p + j*fmt_ori->size; \
                     if ( ma->buf[i].unkn_allele )  /* Use value from the unknown allele when available */ \
                     { \
                         int iunkn = ma->buf[i].unkn_allele; \
-                        for (l=0; l<nsize; l++) { *tgt = src[iunkn]; tgt++; } \
+                        src_type_t val = convert(&src[iunkn * sizeof(src_type_t)]); \
+                        for (l=0; l<nsize; l++) { *tgt = val; tgt++; } \
                     } \
                     else if ( mrule && mrule->type==MERGE_MISSING_CONST ) \
                     { \
@@ -2340,9 +2372,13 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                     } \
                     else if ( mrule && mrule->type==MERGE_MISSING_MAX ) \
                     { \
-                        src = (src_type_t*) fmt_ori->p + j*fmt_ori->n; \
-                        src_type_t max = src[0]; \
-                        for (l=1; l<fmt_ori->n; l++) if ( max < src[l] ) max = src[l]; \
+                        src = fmt_ori->p + sizeof(src_type_t)*j*fmt_ori->n; \
+                        src_type_t max = convert(src); \
+                        for (l=1; l<fmt_ori->n; l++) \
+                        { \
+                            src_type_t val = convert(&src[l * sizeof(src_type_t)]); \
+                            if ( max < val ) max = val; \
+                        } \
                         for (l=0; l<nsize; l++) { *tgt = max; tgt++; } \
                     } \
                     else \
@@ -2356,8 +2392,8 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
                         tgt = (tgt_type_t *) ma->tmp_arr + (ismpl+j)*nsize + inew; \
                         if ( src_is_vector_end ) break; \
                         if ( src_is_missing ) tgt_set_missing; \
-                        else *tgt = *src; \
-                        src++; \
+                        else *tgt = convert(src); \
+                        src += sizeof(src_type_t); \
                     } \
                 } \
             } \
@@ -2365,10 +2401,10 @@ void merge_format_field(args_t *args, bcf_fmt_t **fmt_map, missing_rule_t *mrule
         }
         switch (type)
         {
-            case BCF_BT_INT8:  BRANCH(int32_t,  int8_t, *src==bcf_int8_missing,  *src==bcf_int8_vector_end,  *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
-            case BCF_BT_INT16: BRANCH(int32_t, int16_t, *src==bcf_int16_missing, *src==bcf_int16_vector_end, *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
-            case BCF_BT_INT32: BRANCH(int32_t, int32_t, *src==bcf_int32_missing, *src==bcf_int32_vector_end, *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
-            case BCF_BT_FLOAT: BRANCH(float, float, bcf_float_is_missing(*src), bcf_float_is_vector_end(*src), bcf_float_set_missing(*tgt), bcf_float_set_vector_end(*tgt)); break;
+            case BCF_BT_INT8:  BRANCH(int32_t, int8_t,  le_to_i8,  le_to_i8(src)==bcf_int8_missing,  le_to_i8(src)==bcf_int8_vector_end,  *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
+            case BCF_BT_INT16: BRANCH(int32_t, int16_t, le_to_i16, le_to_i16(src)==bcf_int16_missing, le_to_i16(src)==bcf_int16_vector_end, *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
+            case BCF_BT_INT32: BRANCH(int32_t, int32_t, le_to_i32, le_to_i32(src)==bcf_int32_missing, le_to_i32(src)==bcf_int32_vector_end, *tgt=bcf_int32_missing, *tgt=bcf_int32_vector_end); break;
+            case BCF_BT_FLOAT: BRANCH(float, float, le_to_float, bcf_float_is_missing(le_to_float(src)), bcf_float_is_vector_end(le_to_float(src)), bcf_float_set_missing(*tgt), bcf_float_set_vector_end(*tgt)); break;
             default: error("Unexpected case: %d, %s\n", type, key);
         }
         #undef BRANCH
@@ -2584,10 +2620,20 @@ void gvcf_write_block(args_t *args, int start, int end)
     }
     else
         bcf_update_info_int32(args->out_hdr, out, "END", NULL, 0);
+
+    int iunseen;
+    if ( args->trim_star_allele && (out->n_allele > 2 || args->trim_star_allele > 1) && (iunseen=get_unseen_allele(out)) && iunseen>0 )
+    {
+        // the unobserved star allele should be trimmed, either it is variant site or trimming of all sites was requested
+        kbitset_t *rm_set = kbs_init(out->n_allele);
+        kbs_insert(rm_set, iunseen);
+        if ( bcf_remove_allele_set(args->out_hdr,out,rm_set) )
+            error("[%s] Error: failed to trim the unobserved allele at %s:%"PRIhts_pos"\n",__func__,bcf_seqname(args->out_hdr,out),out->pos+1);
+        kbs_destroy(rm_set);
+    }
     if ( bcf_write1(args->out_fh, args->out_hdr, out)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
     bcf_clear1(out);
 
-
     // Inactivate blocks which do not extend beyond END and find new gvcf_min
     min = INT_MAX;
     for (i=0; i<args->files->nreaders; i++)
@@ -3217,6 +3263,16 @@ void merge_line(args_t *args)
     if ( args->do_gvcf )
         bcf_update_info_int32(args->out_hdr, out, "END", NULL, 0);
     merge_format(args, out);
+    int iunseen;
+    if ( args->trim_star_allele && (out->n_allele > 2 || args->trim_star_allele > 1) && (iunseen=get_unseen_allele(out)) && iunseen>0 )
+    {
+        // the unobserved star allele should be trimmed, either it is variant site or trimming of all sites was requested
+        kbitset_t *rm_set = kbs_init(out->n_allele);
+        kbs_insert(rm_set, iunseen);
+        if ( bcf_remove_allele_set(args->out_hdr,out,rm_set) )
+            error("[%s] Error: failed to trim the unobserved allele at %s:%"PRIhts_pos"\n",__func__,bcf_seqname(args->out_hdr,out),out->pos+1);
+        kbs_destroy(rm_set);
+    }
     if ( bcf_write1(args->out_fh, args->out_hdr, out)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
     bcf_clear1(out);
 }
@@ -3346,9 +3402,11 @@ void merge_vcf(args_t *args)
         if ( hts_close(args->out_fh)!=0 ) error("[%s] Error: close failed .. %s\n", __func__,args->output_fname);
         return;
     }
-    else if ( args->write_index && init_index(args->out_fh,args->out_hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    else if ( init_index2(args->out_fh,args->out_hdr,args->output_fname,
+                          &args->index_fn, args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
 
-    if ( args->collapse==COLLAPSE_NONE ) args->vcmp = vcmp_init();
+    args->vcmp = vcmp_init();
     args->maux = maux_init(args);
     args->out_line = bcf_init1();
     args->tmph = kh_init(strdict);
@@ -3410,17 +3468,19 @@ static void usage(void)
     fprintf(bcftools_stderr, "Usage:   bcftools merge [options] <A.vcf.gz> <B.vcf.gz> [...]\n");
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "Options:\n");
+    fprintf(bcftools_stderr, "        --force-no-index              Merge unindexed files, synonymous to --no-index\n");
     fprintf(bcftools_stderr, "        --force-samples               Resolve duplicate sample names\n");
+    fprintf(bcftools_stderr, "        --force-single                Run even if there is only one file on input\n");
     fprintf(bcftools_stderr, "        --print-header                Print only the merged header and exit\n");
     fprintf(bcftools_stderr, "        --use-header FILE             Use the provided header\n");
     fprintf(bcftools_stderr, "    -0  --missing-to-ref              Assume genotypes at missing sites are 0/0\n");
     fprintf(bcftools_stderr, "    -f, --apply-filters LIST          Require at least one of the listed FILTER strings (e.g. \"PASS,.\")\n");
     fprintf(bcftools_stderr, "    -F, --filter-logic x|+            Remove filters if some input is PASS (\"x\"), or apply all filters (\"+\") [+]\n");
-    fprintf(bcftools_stderr, "    -g, --gvcf -|REF.FA               Merge gVCF blocks, INFO/END tag is expected. Implies -i QS:sum,MinDP:min,I16:sum,IDV:max,IMF:max -M PL:max,AD:0\n");
+    fprintf(bcftools_stderr, "    -g, --gvcf -|REF.FA               Merge gVCF blocks, INFO/END tag is expected. Implies -i QS:sum,MinDP:min,MIN_DP:min,I16:sum,IDV:max,IMF:max -M PL:max,AD:0\n");
     fprintf(bcftools_stderr, "    -i, --info-rules TAG:METHOD,..    Rules for merging INFO fields (method is one of sum,avg,min,max,join) or \"-\" to turn off the default [DP:sum,DP4:sum]\n");
     fprintf(bcftools_stderr, "    -l, --file-list FILE              Read file names from the file\n");
-    fprintf(bcftools_stderr, "    -L, --local-alleles INT           EXPERIMENTAL: if more than <int> ALT alleles are encountered, drop FMT/PL and output LAA+LPL instead; 0=unlimited [0]\n");
-    fprintf(bcftools_stderr, "    -m, --merge STRING                Allow multiallelic records for <snps|indels|both|snp-ins-del|all|none|id>, see man page for details [both]\n");
+    fprintf(bcftools_stderr, "    -L, --local-alleles INT           If more than INT alt alleles are encountered, drop FMT/PL and output LAA+LPL instead; 0=unlimited [0]\n");
+    fprintf(bcftools_stderr, "    -m, --merge STRING[*|**]          Allow multiallelic records for snps,indels,both,snp-ins-del,all,none,id,*,**; see man page for details [both]\n");
     fprintf(bcftools_stderr, "    -M, --missing-rules TAG:METHOD    Rules for replacing missing values in numeric vectors (.,0,max) when unknown allele <*> is not present [.]\n");
     fprintf(bcftools_stderr, "        --no-index                    Merge unindexed files, the same chromosomal order is required and -r/-R are not allowed\n");
     fprintf(bcftools_stderr, "        --no-version                  Do not append version and command line to the header\n");
@@ -3429,8 +3489,8 @@ static void usage(void)
     fprintf(bcftools_stderr, "    -r, --regions REGION              Restrict to comma-separated list of regions\n");
     fprintf(bcftools_stderr, "    -R, --regions-file FILE           Restrict to regions listed in a file\n");
     fprintf(bcftools_stderr, "        --regions-overlap 0|1|2       Include if POS in the region (0), record overlaps (1), variant overlaps (2) [1]\n");
-    fprintf(bcftools_stderr, "        --threads INT                 Use multithreading with <int> worker threads [0]\n");
-    fprintf(bcftools_stderr, "        --write-index                 Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "        --threads INT                 Use multithreading with INT worker threads [0]\n");
+    fprintf(bcftools_stderr, "    -W, --write-index[=FMT]           Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     bcftools_exit(1);
 }
@@ -3472,12 +3532,14 @@ int main_vcfmerge(int argc, char *argv[])
         {"missing-rules",required_argument,NULL,'M'},
         {"no-version",no_argument,NULL,8},
         {"no-index",no_argument,NULL,10},
+        {"force-no-index",no_argument,NULL,10},
+        {"force-single",no_argument,NULL,12},
         {"filter-logic",required_argument,NULL,'F'},
-        {"write-index",no_argument,NULL,11},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "hm:f:r:R:o:O:i:M:l:g:F:0L:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hm:f:r:R:o:O:i:M:l:g:F:0L:W::",loptions,NULL)) >= 0) {
         switch (c) {
             case 'L':
                 args->local_alleles = strtol(optarg,&tmp,10);
@@ -3522,17 +3584,23 @@ int main_vcfmerge(int argc, char *argv[])
                 }
                 break;
             case 'm':
+            {
+                int len = strlen(optarg);
+                if ( optarg[len-1]=='*' ) { args->trim_star_allele++; len--; }
+                if ( optarg[len-1]=='*' ) { args->trim_star_allele++; len--; }
+                if ( optarg[len-1]==',' ) len--;
                 args->collapse = COLLAPSE_NONE;
-                if ( !strcmp(optarg,"snps") ) args->collapse |= COLLAPSE_SNPS;
-                else if ( !strcmp(optarg,"indels") ) args->collapse |= COLLAPSE_INDELS;
-                else if ( !strcmp(optarg,"both") ) args->collapse |= COLLAPSE_BOTH;
-                else if ( !strcmp(optarg,"any") ) args->collapse |= COLLAPSE_ANY;
-                else if ( !strcmp(optarg,"all") ) args->collapse |= COLLAPSE_ANY;
-                else if ( !strcmp(optarg,"none") ) args->collapse = COLLAPSE_NONE;
-                else if ( !strcmp(optarg,"snp-ins-del") ) args->collapse = COLLAPSE_SNP_INS_DEL|COLLAPSE_SNPS;
-                else if ( !strcmp(optarg,"id") ) { args->collapse = COLLAPSE_NONE; args->merge_by_id = 1; }
+                if ( !strncmp(optarg,"snp-ins-del",len) ) args->collapse = COLLAPSE_SNP_INS_DEL|COLLAPSE_SNPS;
+                else if ( !strncmp(optarg,"snps",len) ) args->collapse |= COLLAPSE_SNPS;
+                else if ( !strncmp(optarg,"indels",len) ) args->collapse |= COLLAPSE_INDELS;
+                else if ( !strncmp(optarg,"id",len) ) { args->collapse = COLLAPSE_NONE; args->merge_by_id = 1; }
+                else if ( !strncmp(optarg,"any",len) ) args->collapse |= COLLAPSE_ANY;
+                else if ( !strncmp(optarg,"all",len) ) args->collapse |= COLLAPSE_ANY;
+                else if ( !strncmp(optarg,"both",len) ) args->collapse |= COLLAPSE_BOTH;
+                else if ( !strncmp(optarg,"none",len) ) args->collapse = COLLAPSE_NONE;
                 else error("The -m type \"%s\" is not recognised.\n", optarg);
                 break;
+            }
             case 'f': args->files->apply_filters = optarg; break;
             case 'r': args->regions_list = optarg; break;
             case 'R': args->regions_list = optarg; regions_is_file = 1; break;
@@ -3546,15 +3614,17 @@ int main_vcfmerge(int argc, char *argv[])
             case  9 : args->n_threads = strtol(optarg, 0, 0); break;
             case  8 : args->record_cmd_line = 0; break;
             case 10 : args->no_index = 1; break;
-            case 11 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
+            case 12 : args->force_single = 1; break;
             case 'h':
             case '?': usage(); break;
             default: error("Unknown argument: %s\n", optarg);
         }
     }
     if ( argc==optind && !args->file_list ) usage();
-    if ( argc-optind<2 && !args->file_list ) usage();
-
     if ( args->no_index )
     {
         if ( args->regions_list ) error("Error: cannot combine --no-index with -r/-R\n");
@@ -3595,6 +3665,9 @@ int main_vcfmerge(int argc, char *argv[])
         for (i=0; i<nfiles; i++) free(files[i]);
         free(files);
     }
+    if ( !args->files->nreaders ) usage();
+    if ( args->files->nreaders==1 && !args->force_single ) error("Expected two or more files to merge, got only one. Use --force-single to proceed anyway\n");
+
     merge_vcf(args);
     bcf_sr_destroy(args->files);
     if ( args->regs ) regidx_destroy(args->regs);
index 02ad322d17d496e536ea8ab3718144d2d52ba8cd..f47253385dcc40e04e95a5b304480d4d29ac953f 100644 (file)
@@ -1,6 +1,6 @@
 /*  vcfnorm.c -- Left-align and normalize indels.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -42,6 +42,7 @@ THE SOFTWARE.  */
 #include "abuf.h"
 #include "gff.h"
 #include "regidx.h"
+#include "filter.h"
 
 #define CHECK_REF_EXIT 1
 #define CHECK_REF_WARN 2
@@ -51,6 +52,10 @@ THE SOFTWARE.  */
 #define MROWS_SPLIT 1
 #define MROWS_MERGE  2
 
+// Logic of the filters: include or exclude sites which match the filters?
+#define FLT_INCLUDE 1
+#define FLT_EXCLUDE 2
+
 // for -m+, mapping from allele indexes of a single input record
 // to allele indexes of output record
 typedef struct
@@ -64,7 +69,7 @@ typedef struct
 {
     int n;  // number of alleles
     char *ref, *alt;
-    void *hash;
+    void *hash; // str2int hash
 }
 cmpals1_t;
 
@@ -79,8 +84,8 @@ typedef struct
 {
     char *tseq, *seq;
     int mseq;
-    bcf1_t **lines, **tmp_lines, **alines, **blines, *mrow_out;
-    int ntmp_lines, mtmp_lines, nalines, malines, nblines, mblines;
+    bcf1_t **lines, **tmp_lines, **mrows, *mrow_out;
+    int ntmp_lines, mtmp_lines, nmrows, mmrows, mrows_first;
     map_t *maps;     // mrow map for each buffered record
     char **als;
     int mmaps, nals, mals;
@@ -88,8 +93,8 @@ typedef struct
     int32_t *int32_arr;
     int ntmp_arr1, ntmp_arr2, nint32_arr;
     kstring_t *tmp_str;
-    kstring_t *tmp_als, *tmp_del, tmp_kstr;
-    int ntmp_als, ntmp_del;
+    kstring_t *tmp_als, *tmp_sym, tmp_kstr;
+    int ntmp_als, ntmp_sym;
     rbuf_t rbuf;
     int buf_win;            // maximum distance between two records to consider
     int aln_win;            // the realignment window size (maximum repeat size)
@@ -100,7 +105,7 @@ typedef struct
     struct { int tot, set, swap; } nref;
     char **argv, *output_fname, *ref_fname, *vcf_fname, *region, *targets;
     int argc, rmdup, output_type, n_threads, check_ref, strict_filter, do_indels, clevel;
-    int nchanged, nskipped, nsplit, ntotal, mrows_op, mrows_collapse, parsimonious;
+    int nchanged, nskipped, nsplit, njoined, ntotal, nfilter, mrows_op, mrows_collapse, parsimonious;
     int record_cmd_line, force, force_warned, keep_sum_ad;
     abuf_t *abuf;
     abuf_opt_t atomize;
@@ -108,12 +113,17 @@ typedef struct
     char *old_rec_tag;
     htsFile *out;
     char *index_fn;
-    int write_index;
+    int write_index, gff_verbosity;
     int right_align;
     char *gff_fname;
     gff_t *gff;
     regidx_t *idx_tscript;
     regitr_t *itr_tscript;
+    int (*cmp_func)(const void *aptr, const void *bptr);
+    char *filter_str;
+    int filter_logic;   // include or exclude sites which match the filters? One of FLT_INCLUDE/FLT_EXCLUDE
+    int filter_pass;
+    filter_t *filter;
 }
 args_t;
 
@@ -555,33 +565,57 @@ static int realign(args_t *args, bcf1_t *line)
     if ( bcf_get_variant_types(line)==VCF_BND ) return ERR_SYMBOLIC;   // breakend, not an error
 
     // make a copy of each allele for trimming
-    hts_expand0(kstring_t,line->n_allele,args->ntmp_als,args->tmp_als);
-    hts_expand0(kstring_t,line->n_allele,args->ntmp_del,args->tmp_del);
+    hts_expand0(kstring_t,line->n_allele,args->ntmp_als,args->tmp_als); // the actual sequence to realign
+    hts_expand0(kstring_t,line->n_allele,args->ntmp_sym,args->tmp_sym); // the original symbolic allele strings to output
     kstring_t *als = args->tmp_als;
-    kstring_t *del = args->tmp_del;
+    kstring_t *sym = args->tmp_sym;
+    int symbolic_alts = 1;
     for (i=0; i<line->n_allele; i++)
     {
-        del[i].l = 0;
+        sym[i].l = 0;
         if ( line->d.allele[i][0]=='<' )
         {
-            // symbolic allele, only <DEL.*> will be realigned
-            if ( strncmp("<DEL",line->d.allele[i],4) ) return ERR_SYMBOLIC;
-            if ( nref < line->rlen )
+            // symbolic allele, only <DEL.*> and <DUP.*> will be realigned
+            // TODO: there should be check for symbolic allele length. If too big, perhaps should not attempt realignment
+            int32_t sv_len = 0;
+            if ( !strncmp("<DEL",line->d.allele[i],4) ) sv_len = -line->rlen;
+            else if ( !strncmp("<DUP",line->d.allele[i],4) )
+            {
+                if ( bcf_get_info_int32(args->hdr,line,"SVLEN",&args->int32_arr,&args->nint32_arr)==1 ) sv_len = args->int32_arr[0];
+            }
+            if ( !sv_len ) return ERR_SYMBOLIC;
+
+            als[i].l = 0;
+            if ( sv_len<0 )
+            {
+                // del, expand REF and replace ALT, for example, replace "REF=C ALT=<DEL>" with "REF=CAT ALT=C"
+                if ( nref < line->rlen )
+                {
+                    free(ref);
+                    reflen = line->rlen;
+                    ref = faidx_fetch_seq(args->fai, (char*)args->hdr->id[BCF_DT_CTG][line->rid].key, line->pos, line->pos+reflen-1, &nref);
+                    if ( !ref ) error("faidx_fetch_seq failed at %s:%"PRId64"\n", args->hdr->id[BCF_DT_CTG][line->rid].key, (int64_t) line->pos+1);
+                    seq_to_upper(ref,0);
+                    replace_iupac_codes(ref,nref);  // any non-ACGT character in fasta ref is replaced with N
+                    als[0].l = 0;
+                    kputs(ref, &als[0]);
+                }
+                kputsn(als[0].s,1,&als[i]);
+            }
+            else // sv_len>0
             {
+                // dup, replace "REF=C ALT=<DUP>" with "REF=C ALT=CAT"
                 free(ref);
-                reflen = line->rlen;
-                ref = faidx_fetch_seq(args->fai, (char*)args->hdr->id[BCF_DT_CTG][line->rid].key, line->pos, line->pos+reflen-1, &nref);
+                ref = faidx_fetch_seq(args->fai, (char*)args->hdr->id[BCF_DT_CTG][line->rid].key, line->pos, line->pos+sv_len, &nref);
                 if ( !ref ) error("faidx_fetch_seq failed at %s:%"PRId64"\n", args->hdr->id[BCF_DT_CTG][line->rid].key, (int64_t) line->pos+1);
                 seq_to_upper(ref,0);
                 replace_iupac_codes(ref,nref);  // any non-ACGT character in fasta ref is replaced with N
-                als[0].l = 0;
-                kputs(ref, &als[0]);
-                als[i].l = 0;
-                kputsn(ref,1,&als[i]);
-                kputs(line->d.allele[i],&del[i]);
-                continue;
+                kputs(ref,&als[i]);
             }
+            kputs(line->d.allele[i],&sym[i]);   // preserve the symbolic allele string
+            continue;
         }
+        if ( i>0 ) symbolic_alts = 0;
         if ( line->d.allele[i][0]=='*' ) return ERR_SPANNING_DELETION;  // spanning deletion
         if ( has_non_acgtn(line->d.allele[i],line->shared.l) )
         {
@@ -610,8 +644,15 @@ static int realign(args_t *args, bcf1_t *line)
     else
         new_pos = realign_right(args, line);
 
-    // Have the alleles changed?
-    als[0].s[ als[0].l ] = 0;  // in order for strcmp to work
+    // Have the alleles changed? Consider <DEL> could have expanded the REF allele. In that
+    // case it must be trimmed, however the new REF length must reflect the entire length.
+    als[0].s[ als[0].l ] = 0;   // for strcmp to work
+    int new_reflen = strlen(als[0].s);
+    if ( symbolic_alts )
+    {
+        als[0].l = 1;
+        als[0].s[ als[0].l ] = 0;
+    }
     if ( new_pos==line->pos && !strcasecmp(line->d.allele[0],als[0].s) ) return ERR_OK;
 
     set_old_rec_tag(args, line, line, 0);
@@ -621,7 +662,7 @@ static int realign(args_t *args, bcf1_t *line)
     for (i=0; i<line->n_allele; i++)
     {
         if (i>0) kputc(',',&args->tmp_kstr);
-        if ( del[i].l ) kputs(del[i].s,&args->tmp_kstr);
+        if ( sym[i].l ) kputs(sym[i].s,&args->tmp_kstr);
         else kputsn(als[i].s,als[i].l,&args->tmp_kstr);
     }
     args->tmp_kstr.s[ args->tmp_kstr.l ] = 0;
@@ -629,7 +670,6 @@ static int realign(args_t *args, bcf1_t *line)
     args->nchanged++;
 
     // Update INFO/END if necessary
-    int new_reflen = strlen(line->d.allele[0]);
     if ( (new_pos!=line->pos || reflen!=new_reflen) && bcf_get_info_int32(args->hdr, line, "END", &args->int32_arr, &args->nint32_arr)==1 )
     {
         // bcf_update_alleles_str() messed up rlen because line->pos changed. This will be fixed by bcf_update_info_int32()
@@ -670,7 +710,7 @@ static void split_info_numeric(args_t *args, bcf1_t *src, bcf_info_t *info, int
                     bcf_update_info_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                         tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele-1,ret); \
             } \
             bcf_update_info_##type(args->out_hdr,dst,tag,vals+ialt,1); \
@@ -692,7 +732,7 @@ static void split_info_numeric(args_t *args, bcf1_t *src, bcf_info_t *info, int
                     bcf_update_info_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                         tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele,ret); \
             } \
             if ( args->keep_sum_ad >= 0 && args->keep_sum_ad==info->key ) \
@@ -725,7 +765,7 @@ static void split_info_numeric(args_t *args, bcf1_t *src, bcf_info_t *info, int
                     bcf_update_info_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                         tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele*(src->n_allele+1)/2,ret); \
             } \
             if ( ialt!=0 ) \
@@ -745,31 +785,36 @@ static void split_info_numeric(args_t *args, bcf1_t *src, bcf_info_t *info, int
     }
     #undef BRANCH_NUMERIC
 }
-// Find n-th field in a comma-separated list and move it to dst.
-// The memory areas may overlap.
-#define STR_MOVE_NTH(dst,src,end,nth,len) \
-{ \
-    char *ss = src, *se = src; \
-    int j = 0; \
-    while ( *se && se<(end) ) \
-    { \
-        if ( *se==',' ) \
-        { \
-            if ( j==nth ) break; \
-            j++; \
-            ss = se+1; \
-        } \
-        se++; \
-    } \
-    if ( j==nth ) \
-    { \
-        int n = se - ss; \
-        memmove((dst),ss,n); \
-        src = se; \
-        len += n; \
-    } \
-    else len = -1; \
+// Find nth field in a comma-separated list in src and move it to dst.
+// The dst and src memory areas may overlap, end points just after the last valid src
+// character.
+// On success returns pointer to the end of the parsed field and increments ndst by the
+// number of memmoved characters. Returns NULL if the field was not found.
+static inline char *string_move_nth(char *dst, char *src, char *end, int nth, size_t *ndst)
+{
+    if ( src>=end ) return NULL;
+    char *ss = src, *se = src;
+    int j = 0;
+    while ( *se && se<(end) )
+    {
+        if ( *se==',' )
+        {
+            if ( j==nth ) break;
+            j++;
+            ss = se+1;
+        }
+        se++;
+    }
+    if ( j!=nth ) return NULL;
+    if ( ss>=end ) return NULL;
+    if ( !*ss ) return NULL;
+
+    int n = se - ss;
+    memmove((dst),ss,n);
+    *ndst += n;
+    return se;
 }
+
 static void split_info_string(args_t *args, bcf1_t *src, bcf_info_t *info, int ialt, bcf1_t *dst)
 {
     const char *tag = bcf_hdr_int2id(args->hdr,BCF_DT_ID,info->key);
@@ -784,41 +829,57 @@ static void split_info_string(args_t *args, bcf1_t *src, bcf_info_t *info, int i
     int len = bcf_hdr_id2length(args->hdr,BCF_HL_INFO,info->key);
     if ( len==BCF_VL_A )
     {
+        char *end = str.s + str.l;
         char *tmp = str.s;
-        int len = 0;
-        STR_MOVE_NTH(str.s,tmp,str.s+str.l,ialt,len);
-        if ( len<0 ) return;   // wrong number of fields: skip
-        str.s[len] = 0;
+        str.l = 0;
+        tmp = string_move_nth(str.s,tmp,end,ialt,&str.l);
+        if ( !tmp ) str.l = 1, str.s[0] = '.';
+        kputc_(0,&str);
         bcf_update_info_string(args->out_hdr,dst,tag,str.s);
     }
     else if ( len==BCF_VL_R )
     {
+        char *end = str.s + str.l;
         char *tmp = str.s;
-        int len = 0;
-        STR_MOVE_NTH(str.s,tmp,str.s+str.l,0,len);
-        str.s[len]=','; tmp++; len++;
-        STR_MOVE_NTH(&str.s[len],tmp,str.s+str.l,ialt,len);
-        if ( len<0 ) return;   // wrong number of fields: skip
-        str.s[len] = 0;
+        str.l = 0;
+        tmp = string_move_nth(str.s,tmp,end,0,&str.l);
+        if ( tmp )
+        {
+            kputc_(',',&str);
+            tmp = string_move_nth(str.s+str.l,tmp+1,end,ialt,&str.l); // ialt is 0-based index to ALT
+        }
+        if ( !tmp ) str.l = 1, str.s[0] = '.';
+        kputc_(0,&str);
         bcf_update_info_string(args->out_hdr,dst,tag,str.s);
     }
     else if ( len==BCF_VL_G )
     {
         int i0a = bcf_alleles2gt(0,ialt+1), iaa = bcf_alleles2gt(ialt+1,ialt+1);
+        char *end = str.s + str.l;
         char *tmp = str.s;
-        int len = 0;
-        STR_MOVE_NTH(str.s,tmp,str.s+str.l,0,len);
-        str.s[len]=','; tmp++; len++;
-        STR_MOVE_NTH(&str.s[len],tmp,str.s+str.l,i0a-1,len);
-        if ( len<0 ) return;   // wrong number of fields: skip
-        str.s[len]=','; tmp++; len++;
-        STR_MOVE_NTH(&str.s[len],tmp,str.s+str.l,iaa-i0a-1,len);
-        if ( len<0 ) return;   // wrong number of fields: skip
-        str.s[len] = 0;
+        str.l = 0;
+        tmp = string_move_nth(str.s,tmp,end,0,&str.l);
+        if ( tmp )
+        {
+            kputc_(',',&str);
+            tmp = string_move_nth(str.s+str.l,tmp+1,end,i0a-1,&str.l);
+        }
+        if ( tmp )
+        {
+            kputc_(',',&str);
+            tmp = string_move_nth(str.s+str.l,tmp+1,end,iaa-i0a-1,&str.l);
+        }
+        if ( !tmp ) str.l = 1, str.s[0] = '.';
+        kputc_(0,&str);
         bcf_update_info_string(args->out_hdr,dst,tag,str.s);
     }
     else
         bcf_update_info_string(args->out_hdr,dst,tag,str.s);
+    if ( args->ntmp_arr1 < str.m )
+    {
+        args->ntmp_arr1 = str.m;
+        args->tmp_arr1 = (uint8_t*)str.s;
+    }
 }
 static void split_info_flag(args_t *args, bcf1_t *src, bcf_info_t *info, int ialt, bcf1_t *dst)
 {
@@ -843,7 +904,7 @@ static void split_format_genotype(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int
         {
             if ( gt[j]==bcf_int32_vector_end ) break;
             if ( bcf_gt_is_missing(gt[j]) ) continue; // missing allele: leave as is
-            if ( (ialt==0 || args->ma_use_ref_allele) && bcf_gt_allele(gt[j])==0 ) continue; // ref && `--multi-overlaps 0`: leave as is
+            if ( bcf_gt_allele(gt[j])==0 ) continue; // ref && `--multi-overlaps 0`: leave as is
             if ( bcf_gt_allele(gt[j])==ialt+1 )
                 gt[j] = bcf_gt_unphased(1) | bcf_gt_is_phased(gt[j]); // set to first ALT
             else if ( args->ma_use_ref_allele )
@@ -889,7 +950,7 @@ static void split_format_numeric(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int
                     bcf_update_format_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                     tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,(src->n_allele-1)*nsmpl,nvals); \
             } \
             nvals /= nsmpl; \
@@ -922,7 +983,7 @@ static void split_format_numeric(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int
                     bcf_update_format_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                     tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele*nsmpl,nvals); \
             } \
             nvals /= nsmpl; \
@@ -977,7 +1038,8 @@ static void split_format_numeric(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int
                     bcf_update_format_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error at %s:%"PRId64", the tag %s has wrong number of fields\n", bcf_seqname(args->hdr,src),(int64_t) src->pos+1,bcf_hdr_int2id(args->hdr,BCF_DT_ID,fmt->id)); \
+                error("Error at %s:%"PRId64", the tag %s has wrong number of fields. Use --force to proceed anyway.\n", \
+                    bcf_seqname(args->hdr,src),(int64_t) src->pos+1,bcf_hdr_int2id(args->hdr,BCF_DT_ID,fmt->id)); \
             } \
             nvals /= nsmpl; \
             int all_haploid = nvals==src->n_allele ? 1 : 0; \
@@ -1031,6 +1093,7 @@ static void squeeze_format_char(char *str, int src_blen, int dst_blen, int n)
         isrc += src_blen;
     }
 }
+// ialt is 0-based index to ALT
 static void split_format_string(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int ialt, bcf1_t *dst)
 {
     const char *tag = bcf_hdr_int2id(args->hdr,BCF_DT_ID,fmt->id);
@@ -1038,50 +1101,60 @@ static void split_format_string(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int i
     if ( !ret ) return; // all values can be empty, leave out the tag, no need to panic
     assert( ret>0 );
 
+    int nsmpl = bcf_hdr_nsamples(args->hdr);
+    int blen = ret/nsmpl;   // per-sample field length
+    assert( blen>0 );
+
     kstring_t str;
     str.m = args->ntmp_arr1;
-    str.l = ret;
     str.s = (char*) args->tmp_arr1;
+    str.l = ret;
 
-    int nsmpl = bcf_hdr_nsamples(args->hdr);
-    int len = bcf_hdr_id2length(args->hdr,BCF_HL_FMT,fmt->id);
-    if ( len==BCF_VL_A )
+    int tag_len = bcf_hdr_id2length(args->hdr,BCF_HL_FMT,fmt->id);
+    if ( tag_len==BCF_VL_A )
     {
-        int i, blen = ret/nsmpl, maxlen = 0;
+        int i, maxlen = 0;
         char *ptr = str.s;
         for (i=0; i<nsmpl; i++)
         {
             char *tmp = ptr;
-            int len = 0;
-            STR_MOVE_NTH(tmp,tmp,ptr+blen,ialt,len);
-            if ( len<0 ) return;   // wrong number of fields: skip
+            char *end = ptr + blen;
+            size_t len = 0;
+            tmp = string_move_nth(ptr,tmp,end,ialt,&len);
+            if ( !tmp ) ptr[0] = '.', len = 1;
             if ( maxlen < len ) maxlen = len;
+            while (len<blen) ptr[len++] = 0;
             ptr += blen;
         }
         if ( maxlen<blen ) squeeze_format_char(str.s,blen,maxlen,nsmpl);
         bcf_update_format_char(args->out_hdr,dst,tag,str.s,nsmpl*maxlen);
     }
-    else if ( len==BCF_VL_R )
+    else if ( tag_len==BCF_VL_R )
     {
-        int i, blen = ret/nsmpl, maxlen = 0;
+        int i, maxlen = 0;
         char *ptr = str.s;
         for (i=0; i<nsmpl; i++)
         {
             char *tmp = ptr;
-            int len = 0;
-            STR_MOVE_NTH(ptr,tmp,ptr+blen,0,len);
-            ptr[len]=','; tmp++; len++;
-            STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,ialt,len);
-            if ( len<0 ) return;   // wrong number of fields: skip
+            char *end = ptr + blen;
+            size_t len = 0;
+            tmp = string_move_nth(ptr,tmp,end,0,&len);
+            if ( tmp )
+            {
+                ptr[len++] = ',';
+                tmp = string_move_nth(ptr+len,tmp+1,end,ialt,&len);
+            }
+            if ( !tmp ) ptr[0] = '.', len = 1;
             if ( maxlen < len ) maxlen = len;
+            while (len<blen) ptr[len++] = 0;
             ptr += blen;
         }
         if ( maxlen<blen ) squeeze_format_char(str.s,blen,maxlen,nsmpl);
         bcf_update_format_char(args->out_hdr,dst,tag,str.s,nsmpl*maxlen);
     }
-    else if ( len==BCF_VL_G )
+    else if ( tag_len==BCF_VL_G )
     {
-        int i, blen = ret/nsmpl, maxlen = 0, i0a = bcf_alleles2gt(0,ialt+1), iaa = bcf_alleles2gt(ialt+1,ialt+1);
+        int i, maxlen = 0, i0a = bcf_alleles2gt(0,ialt+1), iaa = bcf_alleles2gt(ialt+1,ialt+1);
         char *ptr = str.s;
         for (i=0; i<nsmpl; i++)
         {
@@ -1108,31 +1181,38 @@ static void split_format_string(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int i
                     bcf_update_format_char(args->out_hdr,dst,tag,NULL,0);
                     return;
                 }
-                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d or %d, found %d\n",
+                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d or %d, found %d. Use --force to proceed anyway.\n",
                         tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele*(src->n_allele+1)/2,src->n_allele,nfields);
             }
 
-            int len = 0;
+            char *tmp = ptr;
+            char *end = ptr + blen;
+            size_t len = 0;
+            tmp = string_move_nth(ptr,tmp,end,0,&len);
             if ( nfields==src->n_allele )   // haploid
             {
-                char *tmp = ptr;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,0,len);
-                ptr[len]=','; tmp++; len++;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,ialt,len);
-                if ( len<0 ) return;   // wrong number of fields: skip
+                if ( tmp )
+                {
+                    ptr[len++] = ',';
+                    tmp = string_move_nth(ptr+len,tmp+1,end,ialt,&len);
+                }
             }
             else    // diploid
             {
-                char *tmp = ptr;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,0,len);
-                ptr[len]=','; tmp++; len++;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,i0a-1,len);
-                if ( len<0 ) return;   // wrong number of fields: skip
-                ptr[len]=','; tmp++; len++;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,iaa-i0a-1,len);
-                if ( len<0 ) return;   // wrong number of fields: skip
+                if ( tmp )
+                {
+                    ptr[len++] = ',';
+                    tmp = string_move_nth(ptr+len,tmp+1,end,i0a-1,&len);
+                }
+                if ( tmp )
+                {
+                    ptr[len++] = ',';
+                    tmp = string_move_nth(ptr+len,tmp+1,end,iaa-i0a-1,&len);
+                }
             }
+            if ( !tmp ) ptr[0] = '.', len = 1;
             if ( maxlen < len ) maxlen = len;
+            while (len<blen) ptr[len++] = 0;
             ptr += blen;
         }
         if ( maxlen<blen ) squeeze_format_char(str.s,blen,maxlen,nsmpl);
@@ -1392,6 +1472,23 @@ static void merge_info_string(args_t *args, bcf1_t **lines, int nlines, bcf_info
         bcf_update_info_string(args->out_hdr,dst,tag,args->tmp_arr1);
     }
 }
+static int gt_array_grow_ploidy(args_t *args, uint8_t **tmp_arr, int *ntmp_arr, int ngt_ori, int ngt_new, int nsmpl)
+{
+    *ntmp_arr = 4*ngt_new*nsmpl;
+    int32_t *ptr = (int32_t*)realloc(*tmp_arr,*ntmp_arr);
+    if ( !ptr ) error("Error: failed to allocate %d bytes\n",*ntmp_arr);
+    *tmp_arr = (uint8_t*) ptr;
+
+    int i,j;
+    for (i=nsmpl-1; i>=0; i--)
+    {
+        int32_t *src = ptr + i*ngt_ori;
+        int32_t *dst = ptr + i*ngt_new;
+        for (j=ngt_new; j>ngt_ori; j--) dst[j-1] = bcf_int32_vector_end;
+        for (j=ngt_ori; j>0; j--) dst[j-1] = src[j-1];
+    }
+    return ngt_new;
+}
 static void merge_format_genotype(args_t *args, bcf1_t **lines, int nlines, bcf_fmt_t *fmt, bcf1_t *dst)
 {
     // reusing int8_t arrays as int32_t arrays
@@ -1410,7 +1507,9 @@ static void merge_format_genotype(args_t *args, bcf1_t **lines, int nlines, bcf_
         int ngts2 = bcf_get_genotypes(args->hdr,lines[i],&args->tmp_arr2,&ntmp2);
         args->ntmp_arr2 = ntmp2 * 4;
         ngts2 /= nsmpl;
-        if ( ngts!=ngts2 ) error("Error at %s:%"PRId64": cannot combine diploid with haploid genotype\n", bcf_seqname(args->hdr,lines[i]),(int64_t) lines[i]->pos+1);
+        int ploidy_changed = ngts - ngts2;
+        if ( ngts < ngts2 ) ngts = gt_array_grow_ploidy(args,&args->tmp_arr1,&args->ntmp_arr1,ngts,ngts2,nsmpl);
+        if ( ngts > ngts2 ) ngts2 = gt_array_grow_ploidy(args,&args->tmp_arr2,&args->ntmp_arr2,ngts2,ngts,nsmpl);
 
         int32_t *gt  = (int32_t*) args->tmp_arr1;       // the first, destination line
         int32_t *gt2 = (int32_t*) args->tmp_arr2;       // one of the subsequent lines, i.e. the source line
@@ -1420,16 +1519,22 @@ static void merge_format_genotype(args_t *args, bcf1_t **lines, int nlines, bcf_
             // never overwrite with ref allele
             for (k2=0; k2<ngts2; k2++)
             {
-                if ( gt2[k2]==bcf_int32_vector_end ) break;
-                if ( bcf_gt_is_missing(gt2[k2]) ) continue;
+                if ( gt2[k2]==bcf_int32_vector_end )
+                {
+                    if ( ploidy_changed && bcf_gt_is_missing(gt[k2]) ) gt[k2] = bcf_int32_vector_end;
+                    break;
+                }
+                if ( bcf_gt_is_missing(gt2[k2]) ) continue;     // don't overwrite with missing
+
+                // don't overwrite with ref, unless the destination is missing, e.g. "./. + 0/1"
                 int ial2 = bcf_gt_allele(gt2[k2]);
-                if ( ial2==0 ) continue;    // never overwrite with ref
+                if ( ial2==0 && !bcf_gt_is_missing(gt[k2]) && gt[k2]!=bcf_int32_vector_end ) continue;
                 if ( ial2>=args->maps[i].nals ) error("Error at %s:%"PRId64": incorrect allele index %d\n",bcf_seqname(args->hdr,lines[i]),(int64_t) lines[i]->pos+1,ial2);
 
                 // The destination allele
                 int ial = args->maps[i].map[ial2];
                 if ( gt[k2]==bcf_int32_vector_end || bcf_gt_is_missing(gt[k2]) || !bcf_gt_allele(gt[k2]) )
-                    gt[k2] = bcf_gt_is_phased(gt[k2]) ? bcf_gt_phased(ial) : bcf_gt_unphased(ial);
+                    gt[k2] = (gt[k2]!=bcf_int32_vector_end && bcf_gt_is_phased(gt[k2])) ? bcf_gt_phased(ial) : bcf_gt_unphased(ial);
                 else
                 {
                     // conflict, the first line has non-zero allele, use the old way, possibly disrupt the phasing
@@ -1822,77 +1927,129 @@ static void merge_biallelics_to_multiallelic(args_t *args, bcf1_t *dst, bcf1_t *
         else if ( type==BCF_HT_INT || type==BCF_HT_REAL ) merge_format_numeric(args, lines, nlines, fmt, dst);
         else merge_format_string(args, lines, nlines, fmt, dst);
     }
+    args->njoined++;
 }
 
 #define SWAP(type_t, a, b) { type_t t = a; a = b; b = t; }
-static void mrows_schedule(args_t *args, bcf1_t **line)
+static void mrows_push(args_t *args, bcf1_t **line)
 {
     int i,m;
-    if ( args->mrows_collapse==COLLAPSE_ANY         // merge all record types together
-        || bcf_get_variant_types(*line)&VCF_SNP     // SNP, put into alines
-        || bcf_get_variant_types(*line)==VCF_REF )  // ref
-    {
-        args->nalines++;
-        m = args->malines;
-        hts_expand(bcf1_t*,args->nalines,args->malines,args->alines);
-        for (i=m; i<args->malines; i++) args->alines[i] = bcf_init1();
-        SWAP(bcf1_t*, args->alines[args->nalines-1], *line);
-    }
-    else
-    {
-        args->nblines++;
-        m = args->mblines;
-        hts_expand(bcf1_t*,args->nblines,args->mblines,args->blines);
-        for (i=m; i<args->mblines; i++) args->blines[i] = bcf_init1();
-        SWAP(bcf1_t*, args->blines[args->nblines-1], *line);
+    if ( !args->nmrows ) args->mrows_first = 0;
+    args->nmrows++;
+    m = args->mmrows;
+    hts_expand(bcf1_t*,args->nmrows,args->mmrows,args->mrows);
+    for (i=m; i<args->mmrows; i++) args->mrows[i] = bcf_init1();
+    SWAP(bcf1_t*, args->mrows[args->nmrows-1], *line);
+
+    if ( args->mrows_collapse==COLLAPSE_ANY ) return;
+
+    // move the line up the sorted list so that the same variant types end up together
+    int cur_type = bcf_get_variant_types(args->mrows[args->nmrows-1]);
+    i = args->mrows_first + args->nmrows - 1;
+    while (i>0)
+    {
+        int prev_type = bcf_get_variant_types(args->mrows[i-1]);
+        if ( prev_type <= cur_type ) break;
+        bcf1_t *tmp = args->mrows[i-1];
+        args->mrows[i-1] = args->mrows[i];
+        args->mrows[i] = tmp;
+        i--;
     }
 }
-static int mrows_ready_to_flush(args_t *args, bcf1_t *line)
+static int mrows_can_flush(args_t *args, bcf1_t *line)
 {
-    if ( args->nalines && (args->alines[0]->rid!=line->rid || args->alines[0]->pos!=line->pos) ) return 1;
-    if ( args->nblines && (args->blines[0]->rid!=line->rid || args->blines[0]->pos!=line->pos) ) return 1;
+    if ( !args->nmrows ) return 0;
+    int ibeg = args->mrows_first;
+    if ( args->mrows[ibeg]->rid != line->rid ) return 1;
+    if ( args->mrows[ibeg]->pos != line->pos ) return 1;
     return 0;
 }
 static bcf1_t *mrows_flush(args_t *args)
 {
-    if ( args->nblines && args->nalines==1 && bcf_get_variant_types(args->alines[0])==VCF_REF )
+    if ( !args->nmrows ) return NULL;
+
+    int ibeg = args->mrows_first;
+
+    //fprintf(stderr,"flush: ibeg=%d n=%d\n",ibeg,args->nmrows);
+    //int i;
+    //for (i=ibeg; i<ibeg+args->nmrows; i++)
+    //  fprintf(stderr,"\ti=%d type=%d %s %s\n",i,bcf_get_variant_types(args->mrows[i]),args->mrows[i]->d.allele[0],args->mrows[i]->d.allele[1]);
+
+    if ( args->nmrows==1 )
     {
-        // By default, REF lines are merged with SNPs if SNPs and indels are to be kept separately.
-        // However, if there are indels only and a single REF line, merge it with indels.
-        args->nblines++;
-        int i,m = args->mblines;
-        hts_expand(bcf1_t*,args->nblines,args->mblines,args->blines);
-        for (i=m; i<args->mblines; i++) args->blines[i] = bcf_init1();
-        SWAP(bcf1_t*, args->blines[args->nblines-1], args->alines[0]);
-        args->nalines--;
+        args->nmrows = 0;
+        return args->mrows[ibeg];
     }
-    if ( args->nalines )
+
+    if ( args->mrows_collapse==COLLAPSE_ANY )
     {
-        if ( args->nalines==1 )
-        {
-            args->nalines = 0;
-            return args->alines[0];
-        }
+        // merge everything with anything
         bcf_clear(args->mrow_out);
-        merge_biallelics_to_multiallelic(args, args->mrow_out, args->alines, args->nalines);
-        args->nalines = 0;
+        merge_biallelics_to_multiallelic(args, args->mrow_out, &args->mrows[ibeg], args->nmrows - ibeg);
+        args->nmrows = 0;
         return args->mrow_out;
     }
-    else if ( args->nblines )
+
+    int j;
+    int types[] = { VCF_SNP, VCF_MNP, VCF_INDEL, VCF_OTHER, -1 };       // merge everything within the same category
+    if ( args->mrows_collapse==COLLAPSE_SNPS ) types[1] = -1;           // merge SNPs only
+    else if ( args->mrows_collapse==COLLAPSE_INDELS ) types[0] = VCF_INDEL, types[1] = -1;    // merge indels only
+    for (j=0; types[j]!=-1; j++)
     {
-        if ( args->nblines==1 )
+        int i, type = types[j]; // to keep the compiler happy
+        for (i=ibeg; i<ibeg+args->nmrows; i++)
         {
-            args->nblines = 0;
-            return args->blines[0];
+            type = bcf_get_variant_types(args->mrows[i]);
+            if ( type!=types[j] && type!=VCF_REF ) break;
+        }
+        if ( i==ibeg+1 && type!=VCF_REF )
+        {
+            // just one line of this type, no merging, but multiple lines of different type follow
+            args->nmrows--;
+            args->mrows_first++;
+            return args->mrows[ibeg];
+        }
+        if ( i>ibeg )
+        {
+            // more than one line, merging is needed
+            int nflush = i - ibeg;
+            bcf_clear(args->mrow_out);
+            merge_biallelics_to_multiallelic(args, args->mrow_out, &args->mrows[ibeg], nflush);
+            args->nmrows -= nflush;
+            args->mrows_first += nflush;
+            return args->mrow_out;
         }
-        bcf_clear(args->mrow_out);
-        merge_biallelics_to_multiallelic(args, args->mrow_out, args->blines, args->nblines);
-        args->nblines = 0;
-        return args->mrow_out;
     }
-    return NULL;
+    args->nmrows--;
+    args->mrows_first++;
+    return args->mrows[ibeg];
 }
-static void cmpals_add(cmpals_t *ca, bcf1_t *rec)
+static char *strdup_alt_svlen(args_t *args, bcf1_t *rec, int ial)
+{
+    if ( rec->d.allele[ial][0]!='<' ) return strdup(rec->d.allele[ial]);
+
+    int ntmp = args->ntmp_arr1 / sizeof(int32_t);
+    int n = bcf_get_info_int32(args->hdr, rec, "SVLEN", &args->tmp_arr1, &ntmp);
+    args->ntmp_arr1 = ntmp * sizeof(int32_t);
+    int32_t *svlen = (int32_t *) args->tmp_arr1;
+    if ( n<=0 ) return strdup(rec->d.allele[ial]);
+
+    if ( n+1 != rec->n_allele )
+    {
+        // there should be as many SVLEN numbers as there are ALT alleles
+        static int warned = 0;
+        if ( !warned )
+        {
+            fprintf(stderr,"TODO: different number of ALT alleles and SVLEN fields %s:%"PRIhts_pos"\n",bcf_seqname(args->hdr,rec),rec->pos+1);
+            warned = 1;
+        }
+    }
+
+    kstring_t str = {0,0,0};
+    ksprintf(&str,"%s.%d",rec->d.allele[ial],svlen[ial-1]);
+    return str.s;
+}
+static void cmpals_add(args_t *args, cmpals_t *ca, bcf1_t *rec)
 {
     ca->ncmpals++;
     hts_expand0(cmpals1_t, ca->ncmpals, ca->mcmpals, ca->cmpals);
@@ -1900,10 +2057,11 @@ static void cmpals_add(cmpals_t *ca, bcf1_t *rec)
     free(cmpals->ref);
     cmpals->ref = strdup(rec->d.allele[0]);
     cmpals->n   = rec->n_allele;
+
     if ( rec->n_allele==2 )
     {
         free(cmpals->alt);
-        cmpals->alt = strdup(rec->d.allele[1]);
+        cmpals->alt = strdup_alt_svlen(args,rec,1);
     }
     else
     {
@@ -1914,9 +2072,10 @@ static void cmpals_add(cmpals_t *ca, bcf1_t *rec)
             khash_str2int_inc(cmpals->hash, strdup(rec->d.allele[i]));
     }
 }
-static int cmpals_match(cmpals_t *ca, bcf1_t *rec)
+static int cmpals_match(args_t *args, cmpals_t *ca, bcf1_t *rec)
 {
     int i, j;
+    char *alt_svlen = rec->n_allele==2 ? strdup_alt_svlen(args,rec,1) : NULL;
     for (i=0; i<ca->ncmpals; i++)
     {
         cmpals1_t *cmpals = ca->cmpals + i;
@@ -1928,7 +2087,8 @@ static int cmpals_match(cmpals_t *ca, bcf1_t *rec)
         // the most frequent case
         if ( rec->n_allele==2 )
         {
-            if ( strcasecmp(rec->d.allele[1], cmpals->alt) ) continue;
+            if ( strcasecmp(alt_svlen, cmpals->alt) ) continue;
+            free(alt_svlen);
             return 1;
         }
 
@@ -1938,6 +2098,7 @@ static int cmpals_match(cmpals_t *ca, bcf1_t *rec)
         if ( j<rec->n_allele ) continue;
         return 1;
     }
+    free(alt_svlen);
     return 0;
 }
 static void cmpals_reset(cmpals_t *ca) { ca->ncmpals = 0; }
@@ -1964,21 +2125,13 @@ static void flush_buffer(args_t *args, htsFile *file, int n)
         k = rbuf_shift(&args->rbuf);
         if ( args->mrows_op==MROWS_MERGE )
         {
-            if ( mrows_ready_to_flush(args, args->lines[k]) )
+            if ( mrows_can_flush(args, args->lines[k]) )
             {
                 while ( (line=mrows_flush(args)) )
                     if ( bcf_write1(file, args->out_hdr, line)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
             }
-            int merge = 1;
-            if ( args->mrows_collapse!=COLLAPSE_BOTH && args->mrows_collapse!=COLLAPSE_ANY )
-            {
-                if ( !(bcf_get_variant_types(args->lines[k]) & args->mrows_collapse) ) merge = 0;
-            }
-            if ( merge )
-            {
-                mrows_schedule(args, &args->lines[k]);
-                continue;
-            }
+            mrows_push(args, &args->lines[k]);
+            continue;
         }
         else if ( args->rmdup )
         {
@@ -1988,7 +2141,7 @@ static void flush_buffer(args_t *args, htsFile *file, int n)
                 if ( args->rmdup & BCF_SR_PAIR_ANY ) continue;    // rmdup by position only
                 if ( args->rmdup & BCF_SR_PAIR_SNPS && line_type&(VCF_SNP|VCF_MNP) && prev_type&(VCF_SNP|VCF_MNP) ) continue;
                 if ( args->rmdup & BCF_SR_PAIR_INDELS && line_type&(VCF_INDEL) && prev_type&(VCF_INDEL) ) continue;
-                if ( args->rmdup & BCF_SR_PAIR_EXACT && cmpals_match(&args->cmpals_out, args->lines[k]) ) continue;
+                if ( args->rmdup & BCF_SR_PAIR_EXACT && cmpals_match(args, &args->cmpals_out, args->lines[k]) ) continue;
             }
             else
             {
@@ -1998,7 +2151,7 @@ static void flush_buffer(args_t *args, htsFile *file, int n)
                 if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_reset(&args->cmpals_out);
             }
             prev_type |= line_type;
-            if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_add(&args->cmpals_out, args->lines[k]);
+            if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_add(args,&args->cmpals_out, args->lines[k]);
         }
         if ( bcf_write1(file, args->out_hdr, args->lines[k])!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
     }
@@ -2020,7 +2173,7 @@ static void init_data(args_t *args)
     else
         args->keep_sum_ad = -1;
 
-    args->out_hdr = bcf_hdr_dup(args->hdr);
+    args->out_hdr = args->hdr;
     if ( args->old_rec_tag )
         bcf_hdr_printf(args->out_hdr,"##INFO=<ID=%s,Number=1,Type=String,Description=\"Original variant. Format: CHR|POS|REF|ALT|USED_ALT_IDX\">",args->old_rec_tag);
 
@@ -2042,22 +2195,31 @@ static void init_data(args_t *args)
         args->abuf = abuf_init(args->hdr, SPLIT);
         abuf_set_opt(args->abuf, bcf_hdr_t*, BCF_HDR, args->out_hdr);
         if ( args->old_rec_tag )
+        {
             abuf_set_opt(args->abuf, const char*, INFO_TAG, args->old_rec_tag);
+            if ( bcf_hdr_sync(args->out_hdr)!=0 ) error("bcf_hdr_sync failed\n");
+        }
         abuf_set_opt(args->abuf, int, STAR_ALLELE, args->use_star_allele);
     }
     if ( args->gff_fname )
     {
         args->gff = gff_init(args->gff_fname);
-        gff_set(args->gff,verbosity,1);
+        gff_set(args->gff,verbosity,args->gff_verbosity);
         gff_set(args->gff,strip_chr_names,1);
         gff_parse(args->gff);
         args->idx_tscript = gff_get(args->gff,idx_tscript);
         args->itr_tscript = regitr_init(NULL);
     }
+    if ( args->filter_str )
+        args->filter = filter_init(args->hdr, args->filter_str);
+    args->filter_pass = 1;
+
+    args->out_hdr = bcf_hdr_dup(args->out_hdr);
 }
 
 static void destroy_data(args_t *args)
 {
+    if ( args->filter ) filter_destroy(args->filter);
     if ( args->gff )
     {
         gff_destroy(args->gff);
@@ -2072,20 +2234,17 @@ static void destroy_data(args_t *args)
     for (i=0; i<args->mtmp_lines; i++)
         if ( args->tmp_lines[i] ) bcf_destroy1(args->tmp_lines[i]);
     free(args->tmp_lines);
-    for (i=0; i<args->malines; i++)
-        bcf_destroy1(args->alines[i]);
-    free(args->alines);
-    for (i=0; i<args->mblines; i++)
-        bcf_destroy1(args->blines[i]);
-    free(args->blines);
+    for (i=0; i<args->mmrows; i++)
+        bcf_destroy1(args->mrows[i]);
+    free(args->mrows);
     for (i=0; i<args->mmaps; i++)
         free(args->maps[i].map);
     for (i=0; i<args->ntmp_als; i++)
         free(args->tmp_als[i].s);
-    for (i=0; i<args->ntmp_del; i++)
-        free(args->tmp_del[i].s);
+    for (i=0; i<args->ntmp_sym; i++)
+        free(args->tmp_sym[i].s);
     free(args->tmp_als);
-    free(args->tmp_del);
+    free(args->tmp_sym);
     free(args->tmp_kstr.s);
     if ( args->tmp_str )
     {
@@ -2110,10 +2269,10 @@ static void normalize_line(args_t *args, bcf1_t *line)
 {
     if ( args->fai )
     {
-        if ( args->check_ref & CHECK_REF_FIX ) fix_ref(args, line);
+        if ( args->filter_pass && (args->check_ref & CHECK_REF_FIX) ) fix_ref(args, line);
         if ( args->do_indels )
         {
-            int ret = realign(args, line);
+            int ret = args->filter_pass ? realign(args, line) : ERR_OK;
 
             // exclude broken VCF lines
             if ( ret==ERR_REF_MISMATCH && args->check_ref & CHECK_REF_SKIP )
@@ -2133,37 +2292,78 @@ static void normalize_line(args_t *args, bcf1_t *line)
         }
     }
 
-    // insert into sorted buffer
-    rbuf_expand0(&args->rbuf,bcf1_t*,args->rbuf.n+1,args->lines);
-    int i,j;
-    i = j = rbuf_append(&args->rbuf);
-    if ( args->lines[i] ) bcf_destroy(args->lines[i]);
-    args->lines[i] = bcf_dup(line);
-    while ( rbuf_prev(&args->rbuf,&i) )
+    if ( args->filter_pass && args->atomize==SPLIT ) abuf_push(args->abuf,line);
+    while (1)
     {
-        if ( args->lines[i]->pos > args->lines[j]->pos ) SWAP(bcf1_t*, args->lines[i], args->lines[j]);
-        j = i;
+        if ( args->filter_pass && args->atomize==SPLIT )
+        {
+            line = abuf_flush(args->abuf, 0);
+            if ( !line ) break;
+        }
+
+        // insert into sorted buffer
+        rbuf_expand0(&args->rbuf,bcf1_t*,args->rbuf.n+1,args->lines);
+        int i,j;
+        i = j = rbuf_append(&args->rbuf);
+        if ( args->lines[i] ) bcf_destroy(args->lines[i]);
+        args->lines[i] = bcf_dup(line);
+        while ( rbuf_prev(&args->rbuf,&i) )
+        {
+            if ( args->lines[i]->rid==args->lines[j]->rid )
+            {
+                bcf_unpack(args->lines[i], BCF_UN_STR);
+                bcf_unpack(args->lines[j], BCF_UN_STR);
+                if ( args->cmp_func(&args->lines[i], &args->lines[j]) > 0) SWAP(bcf1_t*, args->lines[i], args->lines[j]);
+            }
+            j = i;
+        }
+        if ( !args->filter_pass || args->atomize!=SPLIT ) break;
     }
 }
 
-static bcf1_t *next_atomized_line(args_t *args)
+// return 0 on success, 1 when done
+static int split_and_normalize(args_t *args)
 {
-    bcf1_t *rec = NULL;
-    if ( args->atomize==SPLIT )
+    if ( !bcf_sr_next_line(args->files) ) return 1;
+
+    bcf1_t *line = bcf_sr_get_line(args->files,0);
+    args->ntotal++;
+
+    if ( args->filter )
     {
-        rec = abuf_flush(args->abuf, 0);
-        if ( rec ) return rec;
+        args->filter_pass = filter_test(args->filter,line,NULL);
+        if ( args->filter_logic==FLT_EXCLUDE ) args->filter_pass = args->filter_pass ? 0 : 1;
+        if ( !args->filter_pass ) args->nfilter++;
     }
 
-    if ( !bcf_sr_next_line(args->files) ) return NULL;
+    if ( args->mrows_op!=MROWS_SPLIT || line->n_allele<=2 || !args->filter_pass )
+    {
+        // normal operation, no splitting
+        normalize_line(args, line);
+        return 0;
+    }
 
-    if ( args->atomize==SPLIT )
+    // any restrictions on variant types to split?
+    if ( args->mrows_collapse!=COLLAPSE_BOTH && args->mrows_collapse!=COLLAPSE_ANY )
     {
-        abuf_push(args->abuf,bcf_sr_get_line(args->files,0));
-        return abuf_flush(args->abuf, 0);
+        int type = args->mrows_collapse==COLLAPSE_SNPS ? VCF_SNP : VCF_INDEL;
+        if ( !(bcf_get_variant_types(line) & type) )
+        {
+            normalize_line(args, line);
+            return 0;
+        }
     }
-    return bcf_sr_get_line(args->files,0);
+
+    args->nsplit++;
+    split_multiallelic_to_biallelics(args, line);
+
+    int j;
+    for (j=0; j<args->ntmp_lines; j++)
+        normalize_line(args, args->tmp_lines[j]);
+
+    return 0;
 }
+
 static void normalize_vcf(args_t *args)
 {
     char wmode[8];
@@ -2174,64 +2374,40 @@ static void normalize_vcf(args_t *args)
         hts_set_opt(args->out, HTS_OPT_THREAD_POOL, args->files->p);
     if (args->record_cmd_line) bcf_hdr_append_version(args->out_hdr, args->argc, args->argv, "bcftools_norm");
     if ( bcf_hdr_write(args->out, args->out_hdr)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(args->out,args->out_hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( init_index2(args->out,args->out_hdr,args->output_fname,
+                     &args->index_fn, args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
 
-    bcf1_t *line;
-    int prev_rid = -1, prev_pos = -1, prev_type = 0;
-    while ( (line = next_atomized_line(args)) )
+    while (1)
     {
-        args->ntotal++;
-        if ( args->rmdup )
-        {
-            int line_type = bcf_get_variant_types(line);
-            if ( prev_rid>=0 && prev_rid==line->rid && prev_pos==line->pos )
-            {
-                if ( args->rmdup & BCF_SR_PAIR_ANY ) continue;    // rmdup by position only
-                if ( args->rmdup & BCF_SR_PAIR_SNPS && line_type&(VCF_SNP|VCF_MNP) && prev_type&(VCF_SNP|VCF_MNP) ) continue;
-                if ( args->rmdup & BCF_SR_PAIR_INDELS && line_type&(VCF_INDEL) && prev_type&(VCF_INDEL) ) continue;
-                if ( args->rmdup & BCF_SR_PAIR_EXACT && cmpals_match(&args->cmpals_in, line) ) continue;
-            }
-            else
-            {
-                prev_rid  = line->rid;
-                prev_pos  = line->pos;
-                prev_type = 0;
-                if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_reset(&args->cmpals_in);
-            }
-            prev_type |= line_type;
-            if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_add(&args->cmpals_in, line);
-        }
-
-        // still on the same chromosome?
-        int i,j,ilast = rbuf_last(&args->rbuf);
-        if ( ilast>=0 && line->rid != args->lines[ilast]->rid ) flush_buffer(args, args->out, args->rbuf.n); // new chromosome
-
-        int split = 0;
-        if ( args->mrows_op==MROWS_SPLIT )
+        // buffer lines until a different position is reached
+        int done = 0;
+        while (1)
         {
-            split = 1;
-            if ( args->mrows_collapse!=COLLAPSE_BOTH && args->mrows_collapse!=COLLAPSE_ANY )
-            {
-                if ( !(bcf_get_variant_types(line) & args->mrows_collapse) ) split = 0;
-            }
-            if ( split && line->n_allele>2 )
-            {
-                args->nsplit++;
-                split_multiallelic_to_biallelics(args, line);
-                for (j=0; j<args->ntmp_lines; j++)
-                    normalize_line(args, args->tmp_lines[j]);
-            }
-            else
-                split = 0;
+            done = split_and_normalize(args);
+            if ( done ) break;      // no more lines available
+            int i = args->rbuf.f;
+            int j = rbuf_last(&args->rbuf);
+            if ( args->lines[i]->rid != args->lines[j]->rid ) break;
+            if ( args->lines[i]->pos != args->lines[j]->pos ) break;
         }
-        if ( !split )
-            normalize_line(args, line);
+        if ( done ) break;
 
         // find out how many sites to flush
-        ilast = rbuf_last(&args->rbuf);
-        j = 0;
+        int ifst  = args->rbuf.f;
+        int ilast = rbuf_last(&args->rbuf);
+        int i, j = 0;
         for (i=-1; rbuf_next(&args->rbuf,&i); )
         {
+            if ( args->lines[ifst]->rid != args->lines[ilast]->rid )
+            {
+                // there are two chromosomes in the buffer, count how many are on the first chromosome
+                if ( args->lines[ifst]->rid != args->lines[i]->rid ) break;
+                j++;
+                continue;
+            }
+            // there is just one chromosome, flush only lines that are unlikely to change order on
+            // realigning (the buf_win constant)
             if ( args->lines[ilast]->pos - args->lines[i]->pos < args->buf_win ) break;
             j++;
         }
@@ -2249,7 +2425,8 @@ static void normalize_vcf(args_t *args)
     }
     if ( hts_close(args->out)!=0 ) error("[%s] Error: close failed .. %s\n", __func__,args->output_fname);
 
-    fprintf(stderr,"Lines   total/split/realigned/skipped:\t%d/%d/%d/%d\n", args->ntotal,args->nsplit,args->nchanged,args->nskipped);
+    fprintf(stderr,"Lines   total/split/joined/realigned/removed/skipped:\t%d/%d/%d/%d/%d/%d\n",
+        args->ntotal,args->nsplit,args->njoined,args->nchanged,args->nskipped,args->nfilter);
     if ( args->check_ref & CHECK_REF_FIX )
         fprintf(stderr,"REF/ALT total/modified/added:  \t%d/%d/%d\n", args->nref.tot,args->nref.swap,args->nref.set);
 }
@@ -2268,9 +2445,11 @@ static void usage(void)
     fprintf(stderr, "    -c, --check-ref e|w|x|s         Check REF alleles and exit (e), warn (w), exclude (x), or set (s) bad sites [e]\n");
     fprintf(stderr, "    -D, --remove-duplicates         Remove duplicate lines of the same type.\n");
     fprintf(stderr, "    -d, --rm-dup TYPE               Remove duplicate snps|indels|both|all|exact\n");
+    fprintf(stderr, "    -e, --exclude EXPR              Do not normalize records for which the expression is true (see man page for details)\n");
     fprintf(stderr, "    -f, --fasta-ref FILE            Reference sequence\n");
     fprintf(stderr, "        --force                     Try to proceed even if malformed tags are encountered. Experimental, use at your own risk\n");
     fprintf(stderr, "    -g, --gff-annot FILE            Follow HGVS 3'rule and right-align variants in transcripts on the forward strand\n");
+    fprintf(stderr, "    -i, --include EXPR              Normalize only records for which the expression is true (see man page for details)\n");
     fprintf(stderr, "        --keep-sum TAG,..           Keep vector sum constant when splitting multiallelics (see github issue #360)\n");
     fprintf(stderr, "    -m, --multiallelics -|+TYPE     Split multiallelics (-) or join biallelics (+), type: snps|indels|both|any [both]\n");
     fprintf(stderr, "        --multi-overlaps 0|.        Fill in the reference (0) or missing (.) allele when splitting multiallelics [0]\n");
@@ -2283,12 +2462,14 @@ static void usage(void)
     fprintf(stderr, "    -R, --regions-file FILE         Restrict to regions listed in a file\n");
     fprintf(stderr, "        --regions-overlap 0|1|2     Include if POS in the region (0), record overlaps (1), variant overlaps (2) [1]\n");
     fprintf(stderr, "    -s, --strict-filter             When merging (-m+), merged site is PASS only if all sites being merged PASS\n");
+    fprintf(stderr, "    -S, --sort METHOD               Sort order: chr_pos,lex [chr_pos]\n");
     fprintf(stderr, "    -t, --targets REGION            Similar to -r but streams rather than index-jumps\n");
     fprintf(stderr, "    -T, --targets-file FILE         Similar to -R but streams rather than index-jumps\n");
     fprintf(stderr, "        --targets-overlap 0|1|2     Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n");
-    fprintf(stderr, "        --threads INT               Use multithreading with <int> worker threads [0]\n");
+    fprintf(stderr, "        --threads INT               Use multithreading with INT worker threads [0]\n");
+    fprintf(stderr, "    -v, --verbose INT               Verbosity level (0-2) of GFF parsing [1]\n");
     fprintf(stderr, "    -w, --site-win INT              Buffer for sorting lines which changed position during realignment [1000]\n");
-    fprintf(stderr, "        --write-index               Automatically index the output files [off]\n");
+    fprintf(stderr, "    -W, --write-index[=FMT]         Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     fprintf(stderr, "Examples:\n");
     fprintf(stderr, "   # normalize and left-align indels\n");
@@ -2319,8 +2500,10 @@ int main_vcfnorm(int argc, char *argv[])
     int region_is_file  = 0;
     int targets_is_file = 0;
     args->use_star_allele = 1;
+    args->gff_verbosity = 1;
     int regions_overlap = 1;
     int targets_overlap = 0;
+    args->cmp_func = cmp_bcf_pos;
 
     static struct option loptions[] =
     {
@@ -2328,9 +2511,12 @@ int main_vcfnorm(int argc, char *argv[])
         {"force",no_argument,NULL,7},
         {"atomize",no_argument,NULL,'a'},
         {"atom-overlaps",required_argument,NULL,11},
+        {"include",required_argument,NULL,'i'},
+        {"exclude",required_argument,NULL,'e'},
         {"old-rec-tag",required_argument,NULL,12},
         {"keep-sum",required_argument,NULL,10},
         {"fasta-ref",required_argument,NULL,'f'},
+        {"sort",required_argument,NULL,'S'},
         {"gff-annot",required_argument,NULL,'g'},
         {"right-align",no_argument,NULL,15},            // undocumented, only for debugging
         {"do-not-normalize",no_argument,NULL,'N'},
@@ -2351,11 +2537,12 @@ int main_vcfnorm(int argc, char *argv[])
         {"check-ref",required_argument,NULL,'c'},
         {"strict-filter",no_argument,NULL,'s'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,14},
+        {"write-index",optional_argument,NULL,'W'},
+        {"verbose",required_argument,NULL,'v'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "hr:R:f:w:Dd:o:O:c:m:t:T:sNag:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hr:R:f:w:Dd:o:O:c:m:t:T:sNag:W::v:S:i:e:",loptions,NULL)) >= 0) {
         switch (c) {
             case  10:
                 // possibly generalize this also to INFO/AD and other tags
@@ -2364,7 +2551,22 @@ int main_vcfnorm(int argc, char *argv[])
                 args->keep_sum_ad = 1;  // this will be set to the header id or -1 in init_data
                 break;
             case 'g': args->gff_fname = optarg; break;
+            case 'v':
+                args->gff_verbosity = atoi(optarg);
+                if ( args->gff_verbosity<0 || args->gff_verbosity>2 ) error("Error: expected integer 0-2 with -v, --verbose\n");
+                break;
             case 'a': args->atomize = SPLIT; break;
+            case 'e':
+                if ( args->filter_str ) error("Error: only one -i or -e expression can be given, and they cannot be combined\n");
+                args->filter_str = optarg; args->filter_logic |= FLT_EXCLUDE; break;
+            case 'i':
+                if ( args->filter_str ) error("Error: only one -i or -e expression can be given, and they cannot be combined\n");
+                args->filter_str = optarg; args->filter_logic |= FLT_INCLUDE; break;
+            case 'S':
+                if ( !strcasecmp(optarg,"pos") ) args->cmp_func = cmp_bcf_pos;
+                else if ( !strcasecmp(optarg,"lex") ) args->cmp_func = cmp_bcf_pos_ref_alt;
+                else error("Error: the sort order --sort %s is not recognised\n",optarg);
+                break;
             case 11 :
                 if ( optarg[0]=='*' ) args->use_star_allele = 1;
                 else if ( optarg[0]=='.' ) args->use_star_allele = 0;
@@ -2376,7 +2578,10 @@ int main_vcfnorm(int argc, char *argv[])
                 else if ( optarg[0]=='.' ) args->ma_use_ref_allele = 0;
                 else error("Invalid argument to --multi-overlaps\n");
                 break;
-            case 14 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 15 : args->right_align = 1; break;
             case 'N': args->do_indels = 0; break;
             case 'd':
@@ -2485,7 +2690,6 @@ int main_vcfnorm(int argc, char *argv[])
 
     if ( bcf_sr_set_threads(args->files, args->n_threads)<0 ) error("Failed to create threads\n");
     if ( !bcf_sr_add_reader(args->files, fname) ) error("Failed to read from %s: %s\n", !strcmp("-",fname)?"standard input":fname,bcf_sr_strerror(args->files->errnum));
-    if ( args->mrows_op&MROWS_SPLIT && args->rmdup ) error("Cannot combine -D and -m-\n");
     init_data(args);
     normalize_vcf(args);
     destroy_data(args);
index de9c2857b25ca589618779bdd4ea966b721aab0b..4fe92ec9acb2db0dfe9fe6d96aa4a9634c5ff438 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  vcfnorm.c -- Left-align and normalize indels.
 
-    Copyright (C) 2013-2023 Genome Research Ltd.
+    Copyright (C) 2013-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -44,6 +44,7 @@ THE SOFTWARE.  */
 #include "abuf.h"
 #include "gff.h"
 #include "regidx.h"
+#include "filter.h"
 
 #define CHECK_REF_EXIT 1
 #define CHECK_REF_WARN 2
@@ -53,6 +54,10 @@ THE SOFTWARE.  */
 #define MROWS_SPLIT 1
 #define MROWS_MERGE  2
 
+// Logic of the filters: include or exclude sites which match the filters?
+#define FLT_INCLUDE 1
+#define FLT_EXCLUDE 2
+
 // for -m+, mapping from allele indexes of a single input record
 // to allele indexes of output record
 typedef struct
@@ -66,7 +71,7 @@ typedef struct
 {
     int n;  // number of alleles
     char *ref, *alt;
-    void *hash;
+    void *hash; // str2int hash
 }
 cmpals1_t;
 
@@ -81,8 +86,8 @@ typedef struct
 {
     char *tseq, *seq;
     int mseq;
-    bcf1_t **lines, **tmp_lines, **alines, **blines, *mrow_out;
-    int ntmp_lines, mtmp_lines, nalines, malines, nblines, mblines;
+    bcf1_t **lines, **tmp_lines, **mrows, *mrow_out;
+    int ntmp_lines, mtmp_lines, nmrows, mmrows, mrows_first;
     map_t *maps;     // mrow map for each buffered record
     char **als;
     int mmaps, nals, mals;
@@ -90,8 +95,8 @@ typedef struct
     int32_t *int32_arr;
     int ntmp_arr1, ntmp_arr2, nint32_arr;
     kstring_t *tmp_str;
-    kstring_t *tmp_als, *tmp_del, tmp_kstr;
-    int ntmp_als, ntmp_del;
+    kstring_t *tmp_als, *tmp_sym, tmp_kstr;
+    int ntmp_als, ntmp_sym;
     rbuf_t rbuf;
     int buf_win;            // maximum distance between two records to consider
     int aln_win;            // the realignment window size (maximum repeat size)
@@ -102,7 +107,7 @@ typedef struct
     struct { int tot, set, swap; } nref;
     char **argv, *output_fname, *ref_fname, *vcf_fname, *region, *targets;
     int argc, rmdup, output_type, n_threads, check_ref, strict_filter, do_indels, clevel;
-    int nchanged, nskipped, nsplit, ntotal, mrows_op, mrows_collapse, parsimonious;
+    int nchanged, nskipped, nsplit, njoined, ntotal, nfilter, mrows_op, mrows_collapse, parsimonious;
     int record_cmd_line, force, force_warned, keep_sum_ad;
     abuf_t *abuf;
     abuf_opt_t atomize;
@@ -110,12 +115,17 @@ typedef struct
     char *old_rec_tag;
     htsFile *out;
     char *index_fn;
-    int write_index;
+    int write_index, gff_verbosity;
     int right_align;
     char *gff_fname;
     gff_t *gff;
     regidx_t *idx_tscript;
     regitr_t *itr_tscript;
+    int (*cmp_func)(const void *aptr, const void *bptr);
+    char *filter_str;
+    int filter_logic;   // include or exclude sites which match the filters? One of FLT_INCLUDE/FLT_EXCLUDE
+    int filter_pass;
+    filter_t *filter;
 }
 args_t;
 
@@ -557,33 +567,57 @@ static int realign(args_t *args, bcf1_t *line)
     if ( bcf_get_variant_types(line)==VCF_BND ) return ERR_SYMBOLIC;   // breakend, not an error
 
     // make a copy of each allele for trimming
-    hts_expand0(kstring_t,line->n_allele,args->ntmp_als,args->tmp_als);
-    hts_expand0(kstring_t,line->n_allele,args->ntmp_del,args->tmp_del);
+    hts_expand0(kstring_t,line->n_allele,args->ntmp_als,args->tmp_als); // the actual sequence to realign
+    hts_expand0(kstring_t,line->n_allele,args->ntmp_sym,args->tmp_sym); // the original symbolic allele strings to output
     kstring_t *als = args->tmp_als;
-    kstring_t *del = args->tmp_del;
+    kstring_t *sym = args->tmp_sym;
+    int symbolic_alts = 1;
     for (i=0; i<line->n_allele; i++)
     {
-        del[i].l = 0;
+        sym[i].l = 0;
         if ( line->d.allele[i][0]=='<' )
         {
-            // symbolic allele, only <DEL.*> will be realigned
-            if ( strncmp("<DEL",line->d.allele[i],4) ) return ERR_SYMBOLIC;
-            if ( nref < line->rlen )
+            // symbolic allele, only <DEL.*> and <DUP.*> will be realigned
+            // TODO: there should be check for symbolic allele length. If too big, perhaps should not attempt realignment
+            int32_t sv_len = 0;
+            if ( !strncmp("<DEL",line->d.allele[i],4) ) sv_len = -line->rlen;
+            else if ( !strncmp("<DUP",line->d.allele[i],4) )
+            {
+                if ( bcf_get_info_int32(args->hdr,line,"SVLEN",&args->int32_arr,&args->nint32_arr)==1 ) sv_len = args->int32_arr[0];
+            }
+            if ( !sv_len ) return ERR_SYMBOLIC;
+
+            als[i].l = 0;
+            if ( sv_len<0 )
+            {
+                // del, expand REF and replace ALT, for example, replace "REF=C ALT=<DEL>" with "REF=CAT ALT=C"
+                if ( nref < line->rlen )
+                {
+                    free(ref);
+                    reflen = line->rlen;
+                    ref = faidx_fetch_seq(args->fai, (char*)args->hdr->id[BCF_DT_CTG][line->rid].key, line->pos, line->pos+reflen-1, &nref);
+                    if ( !ref ) error("faidx_fetch_seq failed at %s:%"PRId64"\n", args->hdr->id[BCF_DT_CTG][line->rid].key, (int64_t) line->pos+1);
+                    seq_to_upper(ref,0);
+                    replace_iupac_codes(ref,nref);  // any non-ACGT character in fasta ref is replaced with N
+                    als[0].l = 0;
+                    kputs(ref, &als[0]);
+                }
+                kputsn(als[0].s,1,&als[i]);
+            }
+            else // sv_len>0
             {
+                // dup, replace "REF=C ALT=<DUP>" with "REF=C ALT=CAT"
                 free(ref);
-                reflen = line->rlen;
-                ref = faidx_fetch_seq(args->fai, (char*)args->hdr->id[BCF_DT_CTG][line->rid].key, line->pos, line->pos+reflen-1, &nref);
+                ref = faidx_fetch_seq(args->fai, (char*)args->hdr->id[BCF_DT_CTG][line->rid].key, line->pos, line->pos+sv_len, &nref);
                 if ( !ref ) error("faidx_fetch_seq failed at %s:%"PRId64"\n", args->hdr->id[BCF_DT_CTG][line->rid].key, (int64_t) line->pos+1);
                 seq_to_upper(ref,0);
                 replace_iupac_codes(ref,nref);  // any non-ACGT character in fasta ref is replaced with N
-                als[0].l = 0;
-                kputs(ref, &als[0]);
-                als[i].l = 0;
-                kputsn(ref,1,&als[i]);
-                kputs(line->d.allele[i],&del[i]);
-                continue;
+                kputs(ref,&als[i]);
             }
+            kputs(line->d.allele[i],&sym[i]);   // preserve the symbolic allele string
+            continue;
         }
+        if ( i>0 ) symbolic_alts = 0;
         if ( line->d.allele[i][0]=='*' ) return ERR_SPANNING_DELETION;  // spanning deletion
         if ( has_non_acgtn(line->d.allele[i],line->shared.l) )
         {
@@ -612,8 +646,15 @@ static int realign(args_t *args, bcf1_t *line)
     else
         new_pos = realign_right(args, line);
 
-    // Have the alleles changed?
-    als[0].s[ als[0].l ] = 0;  // in order for strcmp to work
+    // Have the alleles changed? Consider <DEL> could have expanded the REF allele. In that
+    // case it must be trimmed, however the new REF length must reflect the entire length.
+    als[0].s[ als[0].l ] = 0;   // for strcmp to work
+    int new_reflen = strlen(als[0].s);
+    if ( symbolic_alts )
+    {
+        als[0].l = 1;
+        als[0].s[ als[0].l ] = 0;
+    }
     if ( new_pos==line->pos && !strcasecmp(line->d.allele[0],als[0].s) ) return ERR_OK;
 
     set_old_rec_tag(args, line, line, 0);
@@ -623,7 +664,7 @@ static int realign(args_t *args, bcf1_t *line)
     for (i=0; i<line->n_allele; i++)
     {
         if (i>0) kputc(',',&args->tmp_kstr);
-        if ( del[i].l ) kputs(del[i].s,&args->tmp_kstr);
+        if ( sym[i].l ) kputs(sym[i].s,&args->tmp_kstr);
         else kputsn(als[i].s,als[i].l,&args->tmp_kstr);
     }
     args->tmp_kstr.s[ args->tmp_kstr.l ] = 0;
@@ -631,7 +672,6 @@ static int realign(args_t *args, bcf1_t *line)
     args->nchanged++;
 
     // Update INFO/END if necessary
-    int new_reflen = strlen(line->d.allele[0]);
     if ( (new_pos!=line->pos || reflen!=new_reflen) && bcf_get_info_int32(args->hdr, line, "END", &args->int32_arr, &args->nint32_arr)==1 )
     {
         // bcf_update_alleles_str() messed up rlen because line->pos changed. This will be fixed by bcf_update_info_int32()
@@ -672,7 +712,7 @@ static void split_info_numeric(args_t *args, bcf1_t *src, bcf_info_t *info, int
                     bcf_update_info_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                         tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele-1,ret); \
             } \
             bcf_update_info_##type(args->out_hdr,dst,tag,vals+ialt,1); \
@@ -694,7 +734,7 @@ static void split_info_numeric(args_t *args, bcf1_t *src, bcf_info_t *info, int
                     bcf_update_info_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                         tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele,ret); \
             } \
             if ( args->keep_sum_ad >= 0 && args->keep_sum_ad==info->key ) \
@@ -727,7 +767,7 @@ static void split_info_numeric(args_t *args, bcf1_t *src, bcf_info_t *info, int
                     bcf_update_info_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in INFO/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                         tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele*(src->n_allele+1)/2,ret); \
             } \
             if ( ialt!=0 ) \
@@ -747,31 +787,36 @@ static void split_info_numeric(args_t *args, bcf1_t *src, bcf_info_t *info, int
     }
     #undef BRANCH_NUMERIC
 }
-// Find n-th field in a comma-separated list and move it to dst.
-// The memory areas may overlap.
-#define STR_MOVE_NTH(dst,src,end,nth,len) \
-{ \
-    char *ss = src, *se = src; \
-    int j = 0; \
-    while ( *se && se<(end) ) \
-    { \
-        if ( *se==',' ) \
-        { \
-            if ( j==nth ) break; \
-            j++; \
-            ss = se+1; \
-        } \
-        se++; \
-    } \
-    if ( j==nth ) \
-    { \
-        int n = se - ss; \
-        memmove((dst),ss,n); \
-        src = se; \
-        len += n; \
-    } \
-    else len = -1; \
+// Find nth field in a comma-separated list in src and move it to dst.
+// The dst and src memory areas may overlap, end points just after the last valid src
+// character.
+// On success returns pointer to the end of the parsed field and increments ndst by the
+// number of memmoved characters. Returns NULL if the field was not found.
+static inline char *string_move_nth(char *dst, char *src, char *end, int nth, size_t *ndst)
+{
+    if ( src>=end ) return NULL;
+    char *ss = src, *se = src;
+    int j = 0;
+    while ( *se && se<(end) )
+    {
+        if ( *se==',' )
+        {
+            if ( j==nth ) break;
+            j++;
+            ss = se+1;
+        }
+        se++;
+    }
+    if ( j!=nth ) return NULL;
+    if ( ss>=end ) return NULL;
+    if ( !*ss ) return NULL;
+
+    int n = se - ss;
+    memmove((dst),ss,n);
+    *ndst += n;
+    return se;
 }
+
 static void split_info_string(args_t *args, bcf1_t *src, bcf_info_t *info, int ialt, bcf1_t *dst)
 {
     const char *tag = bcf_hdr_int2id(args->hdr,BCF_DT_ID,info->key);
@@ -786,41 +831,57 @@ static void split_info_string(args_t *args, bcf1_t *src, bcf_info_t *info, int i
     int len = bcf_hdr_id2length(args->hdr,BCF_HL_INFO,info->key);
     if ( len==BCF_VL_A )
     {
+        char *end = str.s + str.l;
         char *tmp = str.s;
-        int len = 0;
-        STR_MOVE_NTH(str.s,tmp,str.s+str.l,ialt,len);
-        if ( len<0 ) return;   // wrong number of fields: skip
-        str.s[len] = 0;
+        str.l = 0;
+        tmp = string_move_nth(str.s,tmp,end,ialt,&str.l);
+        if ( !tmp ) str.l = 1, str.s[0] = '.';
+        kputc_(0,&str);
         bcf_update_info_string(args->out_hdr,dst,tag,str.s);
     }
     else if ( len==BCF_VL_R )
     {
+        char *end = str.s + str.l;
         char *tmp = str.s;
-        int len = 0;
-        STR_MOVE_NTH(str.s,tmp,str.s+str.l,0,len);
-        str.s[len]=','; tmp++; len++;
-        STR_MOVE_NTH(&str.s[len],tmp,str.s+str.l,ialt,len);
-        if ( len<0 ) return;   // wrong number of fields: skip
-        str.s[len] = 0;
+        str.l = 0;
+        tmp = string_move_nth(str.s,tmp,end,0,&str.l);
+        if ( tmp )
+        {
+            kputc_(',',&str);
+            tmp = string_move_nth(str.s+str.l,tmp+1,end,ialt,&str.l); // ialt is 0-based index to ALT
+        }
+        if ( !tmp ) str.l = 1, str.s[0] = '.';
+        kputc_(0,&str);
         bcf_update_info_string(args->out_hdr,dst,tag,str.s);
     }
     else if ( len==BCF_VL_G )
     {
         int i0a = bcf_alleles2gt(0,ialt+1), iaa = bcf_alleles2gt(ialt+1,ialt+1);
+        char *end = str.s + str.l;
         char *tmp = str.s;
-        int len = 0;
-        STR_MOVE_NTH(str.s,tmp,str.s+str.l,0,len);
-        str.s[len]=','; tmp++; len++;
-        STR_MOVE_NTH(&str.s[len],tmp,str.s+str.l,i0a-1,len);
-        if ( len<0 ) return;   // wrong number of fields: skip
-        str.s[len]=','; tmp++; len++;
-        STR_MOVE_NTH(&str.s[len],tmp,str.s+str.l,iaa-i0a-1,len);
-        if ( len<0 ) return;   // wrong number of fields: skip
-        str.s[len] = 0;
+        str.l = 0;
+        tmp = string_move_nth(str.s,tmp,end,0,&str.l);
+        if ( tmp )
+        {
+            kputc_(',',&str);
+            tmp = string_move_nth(str.s+str.l,tmp+1,end,i0a-1,&str.l);
+        }
+        if ( tmp )
+        {
+            kputc_(',',&str);
+            tmp = string_move_nth(str.s+str.l,tmp+1,end,iaa-i0a-1,&str.l);
+        }
+        if ( !tmp ) str.l = 1, str.s[0] = '.';
+        kputc_(0,&str);
         bcf_update_info_string(args->out_hdr,dst,tag,str.s);
     }
     else
         bcf_update_info_string(args->out_hdr,dst,tag,str.s);
+    if ( args->ntmp_arr1 < str.m )
+    {
+        args->ntmp_arr1 = str.m;
+        args->tmp_arr1 = (uint8_t*)str.s;
+    }
 }
 static void split_info_flag(args_t *args, bcf1_t *src, bcf_info_t *info, int ialt, bcf1_t *dst)
 {
@@ -845,7 +906,7 @@ static void split_format_genotype(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int
         {
             if ( gt[j]==bcf_int32_vector_end ) break;
             if ( bcf_gt_is_missing(gt[j]) ) continue; // missing allele: leave as is
-            if ( (ialt==0 || args->ma_use_ref_allele) && bcf_gt_allele(gt[j])==0 ) continue; // ref && `--multi-overlaps 0`: leave as is
+            if ( bcf_gt_allele(gt[j])==0 ) continue; // ref && `--multi-overlaps 0`: leave as is
             if ( bcf_gt_allele(gt[j])==ialt+1 )
                 gt[j] = bcf_gt_unphased(1) | bcf_gt_is_phased(gt[j]); // set to first ALT
             else if ( args->ma_use_ref_allele )
@@ -891,7 +952,7 @@ static void split_format_numeric(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int
                     bcf_update_format_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                     tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,(src->n_allele-1)*nsmpl,nvals); \
             } \
             nvals /= nsmpl; \
@@ -924,7 +985,7 @@ static void split_format_numeric(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int
                     bcf_update_format_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d, found %d\n", \
+                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d, found %d. Use --force to proceed anyway.\n", \
                     tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele*nsmpl,nvals); \
             } \
             nvals /= nsmpl; \
@@ -979,7 +1040,8 @@ static void split_format_numeric(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int
                     bcf_update_format_##type(args->out_hdr,dst,tag,NULL,0); \
                     return; \
                 } \
-                error("Error at %s:%"PRId64", the tag %s has wrong number of fields\n", bcf_seqname(args->hdr,src),(int64_t) src->pos+1,bcf_hdr_int2id(args->hdr,BCF_DT_ID,fmt->id)); \
+                error("Error at %s:%"PRId64", the tag %s has wrong number of fields. Use --force to proceed anyway.\n", \
+                    bcf_seqname(args->hdr,src),(int64_t) src->pos+1,bcf_hdr_int2id(args->hdr,BCF_DT_ID,fmt->id)); \
             } \
             nvals /= nsmpl; \
             int all_haploid = nvals==src->n_allele ? 1 : 0; \
@@ -1033,6 +1095,7 @@ static void squeeze_format_char(char *str, int src_blen, int dst_blen, int n)
         isrc += src_blen;
     }
 }
+// ialt is 0-based index to ALT
 static void split_format_string(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int ialt, bcf1_t *dst)
 {
     const char *tag = bcf_hdr_int2id(args->hdr,BCF_DT_ID,fmt->id);
@@ -1040,50 +1103,60 @@ static void split_format_string(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int i
     if ( !ret ) return; // all values can be empty, leave out the tag, no need to panic
     assert( ret>0 );
 
+    int nsmpl = bcf_hdr_nsamples(args->hdr);
+    int blen = ret/nsmpl;   // per-sample field length
+    assert( blen>0 );
+
     kstring_t str;
     str.m = args->ntmp_arr1;
-    str.l = ret;
     str.s = (char*) args->tmp_arr1;
+    str.l = ret;
 
-    int nsmpl = bcf_hdr_nsamples(args->hdr);
-    int len = bcf_hdr_id2length(args->hdr,BCF_HL_FMT,fmt->id);
-    if ( len==BCF_VL_A )
+    int tag_len = bcf_hdr_id2length(args->hdr,BCF_HL_FMT,fmt->id);
+    if ( tag_len==BCF_VL_A )
     {
-        int i, blen = ret/nsmpl, maxlen = 0;
+        int i, maxlen = 0;
         char *ptr = str.s;
         for (i=0; i<nsmpl; i++)
         {
             char *tmp = ptr;
-            int len = 0;
-            STR_MOVE_NTH(tmp,tmp,ptr+blen,ialt,len);
-            if ( len<0 ) return;   // wrong number of fields: skip
+            char *end = ptr + blen;
+            size_t len = 0;
+            tmp = string_move_nth(ptr,tmp,end,ialt,&len);
+            if ( !tmp ) ptr[0] = '.', len = 1;
             if ( maxlen < len ) maxlen = len;
+            while (len<blen) ptr[len++] = 0;
             ptr += blen;
         }
         if ( maxlen<blen ) squeeze_format_char(str.s,blen,maxlen,nsmpl);
         bcf_update_format_char(args->out_hdr,dst,tag,str.s,nsmpl*maxlen);
     }
-    else if ( len==BCF_VL_R )
+    else if ( tag_len==BCF_VL_R )
     {
-        int i, blen = ret/nsmpl, maxlen = 0;
+        int i, maxlen = 0;
         char *ptr = str.s;
         for (i=0; i<nsmpl; i++)
         {
             char *tmp = ptr;
-            int len = 0;
-            STR_MOVE_NTH(ptr,tmp,ptr+blen,0,len);
-            ptr[len]=','; tmp++; len++;
-            STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,ialt,len);
-            if ( len<0 ) return;   // wrong number of fields: skip
+            char *end = ptr + blen;
+            size_t len = 0;
+            tmp = string_move_nth(ptr,tmp,end,0,&len);
+            if ( tmp )
+            {
+                ptr[len++] = ',';
+                tmp = string_move_nth(ptr+len,tmp+1,end,ialt,&len);
+            }
+            if ( !tmp ) ptr[0] = '.', len = 1;
             if ( maxlen < len ) maxlen = len;
+            while (len<blen) ptr[len++] = 0;
             ptr += blen;
         }
         if ( maxlen<blen ) squeeze_format_char(str.s,blen,maxlen,nsmpl);
         bcf_update_format_char(args->out_hdr,dst,tag,str.s,nsmpl*maxlen);
     }
-    else if ( len==BCF_VL_G )
+    else if ( tag_len==BCF_VL_G )
     {
-        int i, blen = ret/nsmpl, maxlen = 0, i0a = bcf_alleles2gt(0,ialt+1), iaa = bcf_alleles2gt(ialt+1,ialt+1);
+        int i, maxlen = 0, i0a = bcf_alleles2gt(0,ialt+1), iaa = bcf_alleles2gt(ialt+1,ialt+1);
         char *ptr = str.s;
         for (i=0; i<nsmpl; i++)
         {
@@ -1110,31 +1183,38 @@ static void split_format_string(args_t *args, bcf1_t *src, bcf_fmt_t *fmt, int i
                     bcf_update_format_char(args->out_hdr,dst,tag,NULL,0);
                     return;
                 }
-                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d or %d, found %d\n",
+                error("Error: wrong number of fields in FMT/%s at %s:%"PRId64", expected %d or %d, found %d. Use --force to proceed anyway.\n",
                         tag,bcf_seqname(args->hdr,src),(int64_t) src->pos+1,src->n_allele*(src->n_allele+1)/2,src->n_allele,nfields);
             }
 
-            int len = 0;
+            char *tmp = ptr;
+            char *end = ptr + blen;
+            size_t len = 0;
+            tmp = string_move_nth(ptr,tmp,end,0,&len);
             if ( nfields==src->n_allele )   // haploid
             {
-                char *tmp = ptr;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,0,len);
-                ptr[len]=','; tmp++; len++;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,ialt,len);
-                if ( len<0 ) return;   // wrong number of fields: skip
+                if ( tmp )
+                {
+                    ptr[len++] = ',';
+                    tmp = string_move_nth(ptr+len,tmp+1,end,ialt,&len);
+                }
             }
             else    // diploid
             {
-                char *tmp = ptr;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,0,len);
-                ptr[len]=','; tmp++; len++;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,i0a-1,len);
-                if ( len<0 ) return;   // wrong number of fields: skip
-                ptr[len]=','; tmp++; len++;
-                STR_MOVE_NTH(&ptr[len],tmp,ptr+blen,iaa-i0a-1,len);
-                if ( len<0 ) return;   // wrong number of fields: skip
+                if ( tmp )
+                {
+                    ptr[len++] = ',';
+                    tmp = string_move_nth(ptr+len,tmp+1,end,i0a-1,&len);
+                }
+                if ( tmp )
+                {
+                    ptr[len++] = ',';
+                    tmp = string_move_nth(ptr+len,tmp+1,end,iaa-i0a-1,&len);
+                }
             }
+            if ( !tmp ) ptr[0] = '.', len = 1;
             if ( maxlen < len ) maxlen = len;
+            while (len<blen) ptr[len++] = 0;
             ptr += blen;
         }
         if ( maxlen<blen ) squeeze_format_char(str.s,blen,maxlen,nsmpl);
@@ -1394,6 +1474,23 @@ static void merge_info_string(args_t *args, bcf1_t **lines, int nlines, bcf_info
         bcf_update_info_string(args->out_hdr,dst,tag,args->tmp_arr1);
     }
 }
+static int gt_array_grow_ploidy(args_t *args, uint8_t **tmp_arr, int *ntmp_arr, int ngt_ori, int ngt_new, int nsmpl)
+{
+    *ntmp_arr = 4*ngt_new*nsmpl;
+    int32_t *ptr = (int32_t*)realloc(*tmp_arr,*ntmp_arr);
+    if ( !ptr ) error("Error: failed to allocate %d bytes\n",*ntmp_arr);
+    *tmp_arr = (uint8_t*) ptr;
+
+    int i,j;
+    for (i=nsmpl-1; i>=0; i--)
+    {
+        int32_t *src = ptr + i*ngt_ori;
+        int32_t *dst = ptr + i*ngt_new;
+        for (j=ngt_new; j>ngt_ori; j--) dst[j-1] = bcf_int32_vector_end;
+        for (j=ngt_ori; j>0; j--) dst[j-1] = src[j-1];
+    }
+    return ngt_new;
+}
 static void merge_format_genotype(args_t *args, bcf1_t **lines, int nlines, bcf_fmt_t *fmt, bcf1_t *dst)
 {
     // reusing int8_t arrays as int32_t arrays
@@ -1412,7 +1509,9 @@ static void merge_format_genotype(args_t *args, bcf1_t **lines, int nlines, bcf_
         int ngts2 = bcf_get_genotypes(args->hdr,lines[i],&args->tmp_arr2,&ntmp2);
         args->ntmp_arr2 = ntmp2 * 4;
         ngts2 /= nsmpl;
-        if ( ngts!=ngts2 ) error("Error at %s:%"PRId64": cannot combine diploid with haploid genotype\n", bcf_seqname(args->hdr,lines[i]),(int64_t) lines[i]->pos+1);
+        int ploidy_changed = ngts - ngts2;
+        if ( ngts < ngts2 ) ngts = gt_array_grow_ploidy(args,&args->tmp_arr1,&args->ntmp_arr1,ngts,ngts2,nsmpl);
+        if ( ngts > ngts2 ) ngts2 = gt_array_grow_ploidy(args,&args->tmp_arr2,&args->ntmp_arr2,ngts2,ngts,nsmpl);
 
         int32_t *gt  = (int32_t*) args->tmp_arr1;       // the first, destination line
         int32_t *gt2 = (int32_t*) args->tmp_arr2;       // one of the subsequent lines, i.e. the source line
@@ -1422,16 +1521,22 @@ static void merge_format_genotype(args_t *args, bcf1_t **lines, int nlines, bcf_
             // never overwrite with ref allele
             for (k2=0; k2<ngts2; k2++)
             {
-                if ( gt2[k2]==bcf_int32_vector_end ) break;
-                if ( bcf_gt_is_missing(gt2[k2]) ) continue;
+                if ( gt2[k2]==bcf_int32_vector_end )
+                {
+                    if ( ploidy_changed && bcf_gt_is_missing(gt[k2]) ) gt[k2] = bcf_int32_vector_end;
+                    break;
+                }
+                if ( bcf_gt_is_missing(gt2[k2]) ) continue;     // don't overwrite with missing
+
+                // don't overwrite with ref, unless the destination is missing, e.g. "./. + 0/1"
                 int ial2 = bcf_gt_allele(gt2[k2]);
-                if ( ial2==0 ) continue;    // never overwrite with ref
+                if ( ial2==0 && !bcf_gt_is_missing(gt[k2]) && gt[k2]!=bcf_int32_vector_end ) continue;
                 if ( ial2>=args->maps[i].nals ) error("Error at %s:%"PRId64": incorrect allele index %d\n",bcf_seqname(args->hdr,lines[i]),(int64_t) lines[i]->pos+1,ial2);
 
                 // The destination allele
                 int ial = args->maps[i].map[ial2];
                 if ( gt[k2]==bcf_int32_vector_end || bcf_gt_is_missing(gt[k2]) || !bcf_gt_allele(gt[k2]) )
-                    gt[k2] = bcf_gt_is_phased(gt[k2]) ? bcf_gt_phased(ial) : bcf_gt_unphased(ial);
+                    gt[k2] = (gt[k2]!=bcf_int32_vector_end && bcf_gt_is_phased(gt[k2])) ? bcf_gt_phased(ial) : bcf_gt_unphased(ial);
                 else
                 {
                     // conflict, the first line has non-zero allele, use the old way, possibly disrupt the phasing
@@ -1824,77 +1929,129 @@ static void merge_biallelics_to_multiallelic(args_t *args, bcf1_t *dst, bcf1_t *
         else if ( type==BCF_HT_INT || type==BCF_HT_REAL ) merge_format_numeric(args, lines, nlines, fmt, dst);
         else merge_format_string(args, lines, nlines, fmt, dst);
     }
+    args->njoined++;
 }
 
 #define SWAP(type_t, a, b) { type_t t = a; a = b; b = t; }
-static void mrows_schedule(args_t *args, bcf1_t **line)
+static void mrows_push(args_t *args, bcf1_t **line)
 {
     int i,m;
-    if ( args->mrows_collapse==COLLAPSE_ANY         // merge all record types together
-        || bcf_get_variant_types(*line)&VCF_SNP     // SNP, put into alines
-        || bcf_get_variant_types(*line)==VCF_REF )  // ref
-    {
-        args->nalines++;
-        m = args->malines;
-        hts_expand(bcf1_t*,args->nalines,args->malines,args->alines);
-        for (i=m; i<args->malines; i++) args->alines[i] = bcf_init1();
-        SWAP(bcf1_t*, args->alines[args->nalines-1], *line);
-    }
-    else
-    {
-        args->nblines++;
-        m = args->mblines;
-        hts_expand(bcf1_t*,args->nblines,args->mblines,args->blines);
-        for (i=m; i<args->mblines; i++) args->blines[i] = bcf_init1();
-        SWAP(bcf1_t*, args->blines[args->nblines-1], *line);
+    if ( !args->nmrows ) args->mrows_first = 0;
+    args->nmrows++;
+    m = args->mmrows;
+    hts_expand(bcf1_t*,args->nmrows,args->mmrows,args->mrows);
+    for (i=m; i<args->mmrows; i++) args->mrows[i] = bcf_init1();
+    SWAP(bcf1_t*, args->mrows[args->nmrows-1], *line);
+
+    if ( args->mrows_collapse==COLLAPSE_ANY ) return;
+
+    // move the line up the sorted list so that the same variant types end up together
+    int cur_type = bcf_get_variant_types(args->mrows[args->nmrows-1]);
+    i = args->mrows_first + args->nmrows - 1;
+    while (i>0)
+    {
+        int prev_type = bcf_get_variant_types(args->mrows[i-1]);
+        if ( prev_type <= cur_type ) break;
+        bcf1_t *tmp = args->mrows[i-1];
+        args->mrows[i-1] = args->mrows[i];
+        args->mrows[i] = tmp;
+        i--;
     }
 }
-static int mrows_ready_to_flush(args_t *args, bcf1_t *line)
+static int mrows_can_flush(args_t *args, bcf1_t *line)
 {
-    if ( args->nalines && (args->alines[0]->rid!=line->rid || args->alines[0]->pos!=line->pos) ) return 1;
-    if ( args->nblines && (args->blines[0]->rid!=line->rid || args->blines[0]->pos!=line->pos) ) return 1;
+    if ( !args->nmrows ) return 0;
+    int ibeg = args->mrows_first;
+    if ( args->mrows[ibeg]->rid != line->rid ) return 1;
+    if ( args->mrows[ibeg]->pos != line->pos ) return 1;
     return 0;
 }
 static bcf1_t *mrows_flush(args_t *args)
 {
-    if ( args->nblines && args->nalines==1 && bcf_get_variant_types(args->alines[0])==VCF_REF )
+    if ( !args->nmrows ) return NULL;
+
+    int ibeg = args->mrows_first;
+
+    //fprintf(bcftools_stderr,"flush: ibeg=%d n=%d\n",ibeg,args->nmrows);
+    //int i;
+    //for (i=ibeg; i<ibeg+args->nmrows; i++)
+    //  fprintf(bcftools_stderr,"\ti=%d type=%d %s %s\n",i,bcf_get_variant_types(args->mrows[i]),args->mrows[i]->d.allele[0],args->mrows[i]->d.allele[1]);
+
+    if ( args->nmrows==1 )
     {
-        // By default, REF lines are merged with SNPs if SNPs and indels are to be kept separately.
-        // However, if there are indels only and a single REF line, merge it with indels.
-        args->nblines++;
-        int i,m = args->mblines;
-        hts_expand(bcf1_t*,args->nblines,args->mblines,args->blines);
-        for (i=m; i<args->mblines; i++) args->blines[i] = bcf_init1();
-        SWAP(bcf1_t*, args->blines[args->nblines-1], args->alines[0]);
-        args->nalines--;
+        args->nmrows = 0;
+        return args->mrows[ibeg];
     }
-    if ( args->nalines )
+
+    if ( args->mrows_collapse==COLLAPSE_ANY )
     {
-        if ( args->nalines==1 )
-        {
-            args->nalines = 0;
-            return args->alines[0];
-        }
+        // merge everything with anything
         bcf_clear(args->mrow_out);
-        merge_biallelics_to_multiallelic(args, args->mrow_out, args->alines, args->nalines);
-        args->nalines = 0;
+        merge_biallelics_to_multiallelic(args, args->mrow_out, &args->mrows[ibeg], args->nmrows - ibeg);
+        args->nmrows = 0;
         return args->mrow_out;
     }
-    else if ( args->nblines )
+
+    int j;
+    int types[] = { VCF_SNP, VCF_MNP, VCF_INDEL, VCF_OTHER, -1 };       // merge everything within the same category
+    if ( args->mrows_collapse==COLLAPSE_SNPS ) types[1] = -1;           // merge SNPs only
+    else if ( args->mrows_collapse==COLLAPSE_INDELS ) types[0] = VCF_INDEL, types[1] = -1;    // merge indels only
+    for (j=0; types[j]!=-1; j++)
     {
-        if ( args->nblines==1 )
+        int i, type = types[j]; // to keep the compiler happy
+        for (i=ibeg; i<ibeg+args->nmrows; i++)
         {
-            args->nblines = 0;
-            return args->blines[0];
+            type = bcf_get_variant_types(args->mrows[i]);
+            if ( type!=types[j] && type!=VCF_REF ) break;
+        }
+        if ( i==ibeg+1 && type!=VCF_REF )
+        {
+            // just one line of this type, no merging, but multiple lines of different type follow
+            args->nmrows--;
+            args->mrows_first++;
+            return args->mrows[ibeg];
+        }
+        if ( i>ibeg )
+        {
+            // more than one line, merging is needed
+            int nflush = i - ibeg;
+            bcf_clear(args->mrow_out);
+            merge_biallelics_to_multiallelic(args, args->mrow_out, &args->mrows[ibeg], nflush);
+            args->nmrows -= nflush;
+            args->mrows_first += nflush;
+            return args->mrow_out;
         }
-        bcf_clear(args->mrow_out);
-        merge_biallelics_to_multiallelic(args, args->mrow_out, args->blines, args->nblines);
-        args->nblines = 0;
-        return args->mrow_out;
     }
-    return NULL;
+    args->nmrows--;
+    args->mrows_first++;
+    return args->mrows[ibeg];
 }
-static void cmpals_add(cmpals_t *ca, bcf1_t *rec)
+static char *strdup_alt_svlen(args_t *args, bcf1_t *rec, int ial)
+{
+    if ( rec->d.allele[ial][0]!='<' ) return strdup(rec->d.allele[ial]);
+
+    int ntmp = args->ntmp_arr1 / sizeof(int32_t);
+    int n = bcf_get_info_int32(args->hdr, rec, "SVLEN", &args->tmp_arr1, &ntmp);
+    args->ntmp_arr1 = ntmp * sizeof(int32_t);
+    int32_t *svlen = (int32_t *) args->tmp_arr1;
+    if ( n<=0 ) return strdup(rec->d.allele[ial]);
+
+    if ( n+1 != rec->n_allele )
+    {
+        // there should be as many SVLEN numbers as there are ALT alleles
+        static int warned = 0;
+        if ( !warned )
+        {
+            fprintf(bcftools_stderr,"TODO: different number of ALT alleles and SVLEN fields %s:%"PRIhts_pos"\n",bcf_seqname(args->hdr,rec),rec->pos+1);
+            warned = 1;
+        }
+    }
+
+    kstring_t str = {0,0,0};
+    ksprintf(&str,"%s.%d",rec->d.allele[ial],svlen[ial-1]);
+    return str.s;
+}
+static void cmpals_add(args_t *args, cmpals_t *ca, bcf1_t *rec)
 {
     ca->ncmpals++;
     hts_expand0(cmpals1_t, ca->ncmpals, ca->mcmpals, ca->cmpals);
@@ -1902,10 +2059,11 @@ static void cmpals_add(cmpals_t *ca, bcf1_t *rec)
     free(cmpals->ref);
     cmpals->ref = strdup(rec->d.allele[0]);
     cmpals->n   = rec->n_allele;
+
     if ( rec->n_allele==2 )
     {
         free(cmpals->alt);
-        cmpals->alt = strdup(rec->d.allele[1]);
+        cmpals->alt = strdup_alt_svlen(args,rec,1);
     }
     else
     {
@@ -1916,9 +2074,10 @@ static void cmpals_add(cmpals_t *ca, bcf1_t *rec)
             khash_str2int_inc(cmpals->hash, strdup(rec->d.allele[i]));
     }
 }
-static int cmpals_match(cmpals_t *ca, bcf1_t *rec)
+static int cmpals_match(args_t *args, cmpals_t *ca, bcf1_t *rec)
 {
     int i, j;
+    char *alt_svlen = rec->n_allele==2 ? strdup_alt_svlen(args,rec,1) : NULL;
     for (i=0; i<ca->ncmpals; i++)
     {
         cmpals1_t *cmpals = ca->cmpals + i;
@@ -1930,7 +2089,8 @@ static int cmpals_match(cmpals_t *ca, bcf1_t *rec)
         // the most frequent case
         if ( rec->n_allele==2 )
         {
-            if ( strcasecmp(rec->d.allele[1], cmpals->alt) ) continue;
+            if ( strcasecmp(alt_svlen, cmpals->alt) ) continue;
+            free(alt_svlen);
             return 1;
         }
 
@@ -1940,6 +2100,7 @@ static int cmpals_match(cmpals_t *ca, bcf1_t *rec)
         if ( j<rec->n_allele ) continue;
         return 1;
     }
+    free(alt_svlen);
     return 0;
 }
 static void cmpals_reset(cmpals_t *ca) { ca->ncmpals = 0; }
@@ -1966,21 +2127,13 @@ static void flush_buffer(args_t *args, htsFile *file, int n)
         k = rbuf_shift(&args->rbuf);
         if ( args->mrows_op==MROWS_MERGE )
         {
-            if ( mrows_ready_to_flush(args, args->lines[k]) )
+            if ( mrows_can_flush(args, args->lines[k]) )
             {
                 while ( (line=mrows_flush(args)) )
                     if ( bcf_write1(file, args->out_hdr, line)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
             }
-            int merge = 1;
-            if ( args->mrows_collapse!=COLLAPSE_BOTH && args->mrows_collapse!=COLLAPSE_ANY )
-            {
-                if ( !(bcf_get_variant_types(args->lines[k]) & args->mrows_collapse) ) merge = 0;
-            }
-            if ( merge )
-            {
-                mrows_schedule(args, &args->lines[k]);
-                continue;
-            }
+            mrows_push(args, &args->lines[k]);
+            continue;
         }
         else if ( args->rmdup )
         {
@@ -1990,7 +2143,7 @@ static void flush_buffer(args_t *args, htsFile *file, int n)
                 if ( args->rmdup & BCF_SR_PAIR_ANY ) continue;    // rmdup by position only
                 if ( args->rmdup & BCF_SR_PAIR_SNPS && line_type&(VCF_SNP|VCF_MNP) && prev_type&(VCF_SNP|VCF_MNP) ) continue;
                 if ( args->rmdup & BCF_SR_PAIR_INDELS && line_type&(VCF_INDEL) && prev_type&(VCF_INDEL) ) continue;
-                if ( args->rmdup & BCF_SR_PAIR_EXACT && cmpals_match(&args->cmpals_out, args->lines[k]) ) continue;
+                if ( args->rmdup & BCF_SR_PAIR_EXACT && cmpals_match(args, &args->cmpals_out, args->lines[k]) ) continue;
             }
             else
             {
@@ -2000,7 +2153,7 @@ static void flush_buffer(args_t *args, htsFile *file, int n)
                 if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_reset(&args->cmpals_out);
             }
             prev_type |= line_type;
-            if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_add(&args->cmpals_out, args->lines[k]);
+            if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_add(args,&args->cmpals_out, args->lines[k]);
         }
         if ( bcf_write1(file, args->out_hdr, args->lines[k])!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
     }
@@ -2022,7 +2175,7 @@ static void init_data(args_t *args)
     else
         args->keep_sum_ad = -1;
 
-    args->out_hdr = bcf_hdr_dup(args->hdr);
+    args->out_hdr = args->hdr;
     if ( args->old_rec_tag )
         bcf_hdr_printf(args->out_hdr,"##INFO=<ID=%s,Number=1,Type=String,Description=\"Original variant. Format: CHR|POS|REF|ALT|USED_ALT_IDX\">",args->old_rec_tag);
 
@@ -2044,22 +2197,31 @@ static void init_data(args_t *args)
         args->abuf = abuf_init(args->hdr, SPLIT);
         abuf_set_opt(args->abuf, bcf_hdr_t*, BCF_HDR, args->out_hdr);
         if ( args->old_rec_tag )
+        {
             abuf_set_opt(args->abuf, const char*, INFO_TAG, args->old_rec_tag);
+            if ( bcf_hdr_sync(args->out_hdr)!=0 ) error("bcf_hdr_sync failed\n");
+        }
         abuf_set_opt(args->abuf, int, STAR_ALLELE, args->use_star_allele);
     }
     if ( args->gff_fname )
     {
         args->gff = gff_init(args->gff_fname);
-        gff_set(args->gff,verbosity,1);
+        gff_set(args->gff,verbosity,args->gff_verbosity);
         gff_set(args->gff,strip_chr_names,1);
         gff_parse(args->gff);
         args->idx_tscript = gff_get(args->gff,idx_tscript);
         args->itr_tscript = regitr_init(NULL);
     }
+    if ( args->filter_str )
+        args->filter = filter_init(args->hdr, args->filter_str);
+    args->filter_pass = 1;
+
+    args->out_hdr = bcf_hdr_dup(args->out_hdr);
 }
 
 static void destroy_data(args_t *args)
 {
+    if ( args->filter ) filter_destroy(args->filter);
     if ( args->gff )
     {
         gff_destroy(args->gff);
@@ -2074,20 +2236,17 @@ static void destroy_data(args_t *args)
     for (i=0; i<args->mtmp_lines; i++)
         if ( args->tmp_lines[i] ) bcf_destroy1(args->tmp_lines[i]);
     free(args->tmp_lines);
-    for (i=0; i<args->malines; i++)
-        bcf_destroy1(args->alines[i]);
-    free(args->alines);
-    for (i=0; i<args->mblines; i++)
-        bcf_destroy1(args->blines[i]);
-    free(args->blines);
+    for (i=0; i<args->mmrows; i++)
+        bcf_destroy1(args->mrows[i]);
+    free(args->mrows);
     for (i=0; i<args->mmaps; i++)
         free(args->maps[i].map);
     for (i=0; i<args->ntmp_als; i++)
         free(args->tmp_als[i].s);
-    for (i=0; i<args->ntmp_del; i++)
-        free(args->tmp_del[i].s);
+    for (i=0; i<args->ntmp_sym; i++)
+        free(args->tmp_sym[i].s);
     free(args->tmp_als);
-    free(args->tmp_del);
+    free(args->tmp_sym);
     free(args->tmp_kstr.s);
     if ( args->tmp_str )
     {
@@ -2112,10 +2271,10 @@ static void normalize_line(args_t *args, bcf1_t *line)
 {
     if ( args->fai )
     {
-        if ( args->check_ref & CHECK_REF_FIX ) fix_ref(args, line);
+        if ( args->filter_pass && (args->check_ref & CHECK_REF_FIX) ) fix_ref(args, line);
         if ( args->do_indels )
         {
-            int ret = realign(args, line);
+            int ret = args->filter_pass ? realign(args, line) : ERR_OK;
 
             // exclude broken VCF lines
             if ( ret==ERR_REF_MISMATCH && args->check_ref & CHECK_REF_SKIP )
@@ -2135,37 +2294,78 @@ static void normalize_line(args_t *args, bcf1_t *line)
         }
     }
 
-    // insert into sorted buffer
-    rbuf_expand0(&args->rbuf,bcf1_t*,args->rbuf.n+1,args->lines);
-    int i,j;
-    i = j = rbuf_append(&args->rbuf);
-    if ( args->lines[i] ) bcf_destroy(args->lines[i]);
-    args->lines[i] = bcf_dup(line);
-    while ( rbuf_prev(&args->rbuf,&i) )
+    if ( args->filter_pass && args->atomize==SPLIT ) abuf_push(args->abuf,line);
+    while (1)
     {
-        if ( args->lines[i]->pos > args->lines[j]->pos ) SWAP(bcf1_t*, args->lines[i], args->lines[j]);
-        j = i;
+        if ( args->filter_pass && args->atomize==SPLIT )
+        {
+            line = abuf_flush(args->abuf, 0);
+            if ( !line ) break;
+        }
+
+        // insert into sorted buffer
+        rbuf_expand0(&args->rbuf,bcf1_t*,args->rbuf.n+1,args->lines);
+        int i,j;
+        i = j = rbuf_append(&args->rbuf);
+        if ( args->lines[i] ) bcf_destroy(args->lines[i]);
+        args->lines[i] = bcf_dup(line);
+        while ( rbuf_prev(&args->rbuf,&i) )
+        {
+            if ( args->lines[i]->rid==args->lines[j]->rid )
+            {
+                bcf_unpack(args->lines[i], BCF_UN_STR);
+                bcf_unpack(args->lines[j], BCF_UN_STR);
+                if ( args->cmp_func(&args->lines[i], &args->lines[j]) > 0) SWAP(bcf1_t*, args->lines[i], args->lines[j]);
+            }
+            j = i;
+        }
+        if ( !args->filter_pass || args->atomize!=SPLIT ) break;
     }
 }
 
-static bcf1_t *next_atomized_line(args_t *args)
+// return 0 on success, 1 when done
+static int split_and_normalize(args_t *args)
 {
-    bcf1_t *rec = NULL;
-    if ( args->atomize==SPLIT )
+    if ( !bcf_sr_next_line(args->files) ) return 1;
+
+    bcf1_t *line = bcf_sr_get_line(args->files,0);
+    args->ntotal++;
+
+    if ( args->filter )
     {
-        rec = abuf_flush(args->abuf, 0);
-        if ( rec ) return rec;
+        args->filter_pass = filter_test(args->filter,line,NULL);
+        if ( args->filter_logic==FLT_EXCLUDE ) args->filter_pass = args->filter_pass ? 0 : 1;
+        if ( !args->filter_pass ) args->nfilter++;
     }
 
-    if ( !bcf_sr_next_line(args->files) ) return NULL;
+    if ( args->mrows_op!=MROWS_SPLIT || line->n_allele<=2 || !args->filter_pass )
+    {
+        // normal operation, no splitting
+        normalize_line(args, line);
+        return 0;
+    }
 
-    if ( args->atomize==SPLIT )
+    // any restrictions on variant types to split?
+    if ( args->mrows_collapse!=COLLAPSE_BOTH && args->mrows_collapse!=COLLAPSE_ANY )
     {
-        abuf_push(args->abuf,bcf_sr_get_line(args->files,0));
-        return abuf_flush(args->abuf, 0);
+        int type = args->mrows_collapse==COLLAPSE_SNPS ? VCF_SNP : VCF_INDEL;
+        if ( !(bcf_get_variant_types(line) & type) )
+        {
+            normalize_line(args, line);
+            return 0;
+        }
     }
-    return bcf_sr_get_line(args->files,0);
+
+    args->nsplit++;
+    split_multiallelic_to_biallelics(args, line);
+
+    int j;
+    for (j=0; j<args->ntmp_lines; j++)
+        normalize_line(args, args->tmp_lines[j]);
+
+    return 0;
 }
+
 static void normalize_vcf(args_t *args)
 {
     char wmode[8];
@@ -2176,64 +2376,40 @@ static void normalize_vcf(args_t *args)
         hts_set_opt(args->out, HTS_OPT_THREAD_POOL, args->files->p);
     if (args->record_cmd_line) bcf_hdr_append_version(args->out_hdr, args->argc, args->argv, "bcftools_norm");
     if ( bcf_hdr_write(args->out, args->out_hdr)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(args->out,args->out_hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( init_index2(args->out,args->out_hdr,args->output_fname,
+                     &args->index_fn, args->write_index)<0 )
+        error("Error: failed to initialise index for %s\n",args->output_fname);
 
-    bcf1_t *line;
-    int prev_rid = -1, prev_pos = -1, prev_type = 0;
-    while ( (line = next_atomized_line(args)) )
+    while (1)
     {
-        args->ntotal++;
-        if ( args->rmdup )
-        {
-            int line_type = bcf_get_variant_types(line);
-            if ( prev_rid>=0 && prev_rid==line->rid && prev_pos==line->pos )
-            {
-                if ( args->rmdup & BCF_SR_PAIR_ANY ) continue;    // rmdup by position only
-                if ( args->rmdup & BCF_SR_PAIR_SNPS && line_type&(VCF_SNP|VCF_MNP) && prev_type&(VCF_SNP|VCF_MNP) ) continue;
-                if ( args->rmdup & BCF_SR_PAIR_INDELS && line_type&(VCF_INDEL) && prev_type&(VCF_INDEL) ) continue;
-                if ( args->rmdup & BCF_SR_PAIR_EXACT && cmpals_match(&args->cmpals_in, line) ) continue;
-            }
-            else
-            {
-                prev_rid  = line->rid;
-                prev_pos  = line->pos;
-                prev_type = 0;
-                if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_reset(&args->cmpals_in);
-            }
-            prev_type |= line_type;
-            if ( args->rmdup & BCF_SR_PAIR_EXACT ) cmpals_add(&args->cmpals_in, line);
-        }
-
-        // still on the same chromosome?
-        int i,j,ilast = rbuf_last(&args->rbuf);
-        if ( ilast>=0 && line->rid != args->lines[ilast]->rid ) flush_buffer(args, args->out, args->rbuf.n); // new chromosome
-
-        int split = 0;
-        if ( args->mrows_op==MROWS_SPLIT )
+        // buffer lines until a different position is reached
+        int done = 0;
+        while (1)
         {
-            split = 1;
-            if ( args->mrows_collapse!=COLLAPSE_BOTH && args->mrows_collapse!=COLLAPSE_ANY )
-            {
-                if ( !(bcf_get_variant_types(line) & args->mrows_collapse) ) split = 0;
-            }
-            if ( split && line->n_allele>2 )
-            {
-                args->nsplit++;
-                split_multiallelic_to_biallelics(args, line);
-                for (j=0; j<args->ntmp_lines; j++)
-                    normalize_line(args, args->tmp_lines[j]);
-            }
-            else
-                split = 0;
+            done = split_and_normalize(args);
+            if ( done ) break;      // no more lines available
+            int i = args->rbuf.f;
+            int j = rbuf_last(&args->rbuf);
+            if ( args->lines[i]->rid != args->lines[j]->rid ) break;
+            if ( args->lines[i]->pos != args->lines[j]->pos ) break;
         }
-        if ( !split )
-            normalize_line(args, line);
+        if ( done ) break;
 
         // find out how many sites to flush
-        ilast = rbuf_last(&args->rbuf);
-        j = 0;
+        int ifst  = args->rbuf.f;
+        int ilast = rbuf_last(&args->rbuf);
+        int i, j = 0;
         for (i=-1; rbuf_next(&args->rbuf,&i); )
         {
+            if ( args->lines[ifst]->rid != args->lines[ilast]->rid )
+            {
+                // there are two chromosomes in the buffer, count how many are on the first chromosome
+                if ( args->lines[ifst]->rid != args->lines[i]->rid ) break;
+                j++;
+                continue;
+            }
+            // there is just one chromosome, flush only lines that are unlikely to change order on
+            // realigning (the buf_win constant)
             if ( args->lines[ilast]->pos - args->lines[i]->pos < args->buf_win ) break;
             j++;
         }
@@ -2251,7 +2427,8 @@ static void normalize_vcf(args_t *args)
     }
     if ( hts_close(args->out)!=0 ) error("[%s] Error: close failed .. %s\n", __func__,args->output_fname);
 
-    fprintf(bcftools_stderr,"Lines   total/split/realigned/skipped:\t%d/%d/%d/%d\n", args->ntotal,args->nsplit,args->nchanged,args->nskipped);
+    fprintf(bcftools_stderr,"Lines   total/split/joined/realigned/removed/skipped:\t%d/%d/%d/%d/%d/%d\n",
+        args->ntotal,args->nsplit,args->njoined,args->nchanged,args->nskipped,args->nfilter);
     if ( args->check_ref & CHECK_REF_FIX )
         fprintf(bcftools_stderr,"REF/ALT total/modified/added:  \t%d/%d/%d\n", args->nref.tot,args->nref.swap,args->nref.set);
 }
@@ -2270,9 +2447,11 @@ static void usage(void)
     fprintf(bcftools_stderr, "    -c, --check-ref e|w|x|s         Check REF alleles and exit (e), warn (w), exclude (x), or set (s) bad sites [e]\n");
     fprintf(bcftools_stderr, "    -D, --remove-duplicates         Remove duplicate lines of the same type.\n");
     fprintf(bcftools_stderr, "    -d, --rm-dup TYPE               Remove duplicate snps|indels|both|all|exact\n");
+    fprintf(bcftools_stderr, "    -e, --exclude EXPR              Do not normalize records for which the expression is true (see man page for details)\n");
     fprintf(bcftools_stderr, "    -f, --fasta-ref FILE            Reference sequence\n");
     fprintf(bcftools_stderr, "        --force                     Try to proceed even if malformed tags are encountered. Experimental, use at your own risk\n");
     fprintf(bcftools_stderr, "    -g, --gff-annot FILE            Follow HGVS 3'rule and right-align variants in transcripts on the forward strand\n");
+    fprintf(bcftools_stderr, "    -i, --include EXPR              Normalize only records for which the expression is true (see man page for details)\n");
     fprintf(bcftools_stderr, "        --keep-sum TAG,..           Keep vector sum constant when splitting multiallelics (see github issue #360)\n");
     fprintf(bcftools_stderr, "    -m, --multiallelics -|+TYPE     Split multiallelics (-) or join biallelics (+), type: snps|indels|both|any [both]\n");
     fprintf(bcftools_stderr, "        --multi-overlaps 0|.        Fill in the reference (0) or missing (.) allele when splitting multiallelics [0]\n");
@@ -2285,12 +2464,14 @@ static void usage(void)
     fprintf(bcftools_stderr, "    -R, --regions-file FILE         Restrict to regions listed in a file\n");
     fprintf(bcftools_stderr, "        --regions-overlap 0|1|2     Include if POS in the region (0), record overlaps (1), variant overlaps (2) [1]\n");
     fprintf(bcftools_stderr, "    -s, --strict-filter             When merging (-m+), merged site is PASS only if all sites being merged PASS\n");
+    fprintf(bcftools_stderr, "    -S, --sort METHOD               Sort order: chr_pos,lex [chr_pos]\n");
     fprintf(bcftools_stderr, "    -t, --targets REGION            Similar to -r but streams rather than index-jumps\n");
     fprintf(bcftools_stderr, "    -T, --targets-file FILE         Similar to -R but streams rather than index-jumps\n");
     fprintf(bcftools_stderr, "        --targets-overlap 0|1|2     Include if POS in the region (0), record overlaps (1), variant overlaps (2) [0]\n");
-    fprintf(bcftools_stderr, "        --threads INT               Use multithreading with <int> worker threads [0]\n");
+    fprintf(bcftools_stderr, "        --threads INT               Use multithreading with INT worker threads [0]\n");
+    fprintf(bcftools_stderr, "    -v, --verbose INT               Verbosity level (0-2) of GFF parsing [1]\n");
     fprintf(bcftools_stderr, "    -w, --site-win INT              Buffer for sorting lines which changed position during realignment [1000]\n");
-    fprintf(bcftools_stderr, "        --write-index               Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "    -W, --write-index[=FMT]         Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "Examples:\n");
     fprintf(bcftools_stderr, "   # normalize and left-align indels\n");
@@ -2321,8 +2502,10 @@ int main_vcfnorm(int argc, char *argv[])
     int region_is_file  = 0;
     int targets_is_file = 0;
     args->use_star_allele = 1;
+    args->gff_verbosity = 1;
     int regions_overlap = 1;
     int targets_overlap = 0;
+    args->cmp_func = cmp_bcf_pos;
 
     static struct option loptions[] =
     {
@@ -2330,9 +2513,12 @@ int main_vcfnorm(int argc, char *argv[])
         {"force",no_argument,NULL,7},
         {"atomize",no_argument,NULL,'a'},
         {"atom-overlaps",required_argument,NULL,11},
+        {"include",required_argument,NULL,'i'},
+        {"exclude",required_argument,NULL,'e'},
         {"old-rec-tag",required_argument,NULL,12},
         {"keep-sum",required_argument,NULL,10},
         {"fasta-ref",required_argument,NULL,'f'},
+        {"sort",required_argument,NULL,'S'},
         {"gff-annot",required_argument,NULL,'g'},
         {"right-align",no_argument,NULL,15},            // undocumented, only for debugging
         {"do-not-normalize",no_argument,NULL,'N'},
@@ -2353,11 +2539,12 @@ int main_vcfnorm(int argc, char *argv[])
         {"check-ref",required_argument,NULL,'c'},
         {"strict-filter",no_argument,NULL,'s'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,14},
+        {"write-index",optional_argument,NULL,'W'},
+        {"verbose",required_argument,NULL,'v'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "hr:R:f:w:Dd:o:O:c:m:t:T:sNag:",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hr:R:f:w:Dd:o:O:c:m:t:T:sNag:W::v:S:i:e:",loptions,NULL)) >= 0) {
         switch (c) {
             case  10:
                 // possibly generalize this also to INFO/AD and other tags
@@ -2366,7 +2553,22 @@ int main_vcfnorm(int argc, char *argv[])
                 args->keep_sum_ad = 1;  // this will be set to the header id or -1 in init_data
                 break;
             case 'g': args->gff_fname = optarg; break;
+            case 'v':
+                args->gff_verbosity = atoi(optarg);
+                if ( args->gff_verbosity<0 || args->gff_verbosity>2 ) error("Error: expected integer 0-2 with -v, --verbose\n");
+                break;
             case 'a': args->atomize = SPLIT; break;
+            case 'e':
+                if ( args->filter_str ) error("Error: only one -i or -e expression can be given, and they cannot be combined\n");
+                args->filter_str = optarg; args->filter_logic |= FLT_EXCLUDE; break;
+            case 'i':
+                if ( args->filter_str ) error("Error: only one -i or -e expression can be given, and they cannot be combined\n");
+                args->filter_str = optarg; args->filter_logic |= FLT_INCLUDE; break;
+            case 'S':
+                if ( !strcasecmp(optarg,"pos") ) args->cmp_func = cmp_bcf_pos;
+                else if ( !strcasecmp(optarg,"lex") ) args->cmp_func = cmp_bcf_pos_ref_alt;
+                else error("Error: the sort order --sort %s is not recognised\n",optarg);
+                break;
             case 11 :
                 if ( optarg[0]=='*' ) args->use_star_allele = 1;
                 else if ( optarg[0]=='.' ) args->use_star_allele = 0;
@@ -2378,7 +2580,10 @@ int main_vcfnorm(int argc, char *argv[])
                 else if ( optarg[0]=='.' ) args->ma_use_ref_allele = 0;
                 else error("Invalid argument to --multi-overlaps\n");
                 break;
-            case 14 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 15 : args->right_align = 1; break;
             case 'N': args->do_indels = 0; break;
             case 'd':
@@ -2487,7 +2692,6 @@ int main_vcfnorm(int argc, char *argv[])
 
     if ( bcf_sr_set_threads(args->files, args->n_threads)<0 ) error("Failed to create threads\n");
     if ( !bcf_sr_add_reader(args->files, fname) ) error("Failed to read from %s: %s\n", !strcmp("-",fname)?"standard input":fname,bcf_sr_strerror(args->files->errnum));
-    if ( args->mrows_op&MROWS_SPLIT && args->rmdup ) error("Cannot combine -D and -m-\n");
     init_data(args);
     normalize_vcf(args);
     destroy_data(args);
index 68775196116047e24e5082bb6cd6995d0e839a7b..4ee99ee13c825d5992a0e1b106920ff06a15bbe2 100644 (file)
@@ -66,7 +66,7 @@ typedef struct _plugin_t plugin_t;
  *      success or non-zero value on error.
  *
  *   int init(int argc, char **argv, bcf_hdr_t *in_hdr, bcf_hdr_t *out_hdr)
- *      - called once at startup, allows to initialize local variables.
+ *      - called once at startup, it initializes local variables.
  *      Return 1 to suppress normal VCF/BCF header output, -1 on critical
  *      errors, 0 otherwise.
  *
@@ -550,7 +550,9 @@ static void init_data(args_t *args)
         if ( args->out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->output_fname, strerror(errno));
         if ( args->n_threads ) hts_set_threads(args->out_fh, args->n_threads);
         if ( bcf_hdr_write(args->out_fh, args->hdr_out)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
-        if ( args->write_index && init_index(args->out_fh,args->hdr_out,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+        if ( init_index2(args->out_fh,args->hdr_out,args->output_fname,
+                         &args->index_fn, args->write_index)<0 )
+            error("Error: failed to initialise index for %s\n",args->output_fname);
     }
 }
 
@@ -613,7 +615,7 @@ static void usage(args_t *args)
     fprintf(stderr, "   -l, --list-plugins             List available plugins. See BCFTOOLS_PLUGINS environment variable and man page for details\n");
     fprintf(stderr, "   -v, --verbose                  Print verbose information, -vv increases verbosity\n");
     fprintf(stderr, "   -V, --version                  Print version string and exit\n");
-    fprintf(stderr, "       --write-index              Automatically index the output files [off]\n");
+    fprintf(stderr, "   -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     exit(1);
 }
@@ -691,11 +693,11 @@ int main_plugin(int argc, char *argv[])
         {"targets-file",required_argument,NULL,'T'},
         {"targets-overlap",required_argument,NULL,2},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,10},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "h?o:O:r:R:t:T:li:e:vV",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h?o:O:r:R:t:T:li:e:vVW::",loptions,NULL)) >= 0)
     {
         switch (c) {
             case 'V': version_only = 1; break;
@@ -740,7 +742,10 @@ int main_plugin(int argc, char *argv[])
                 break;
             case  9 : args->n_threads = strtol(optarg, 0, 0); break;
             case  8 : args->record_cmd_line = 0; break;
-            case 10 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case '?':
             case 'h': usage_only = 1; break;
             default: error("Unknown argument: %s\n", optarg);
index ad04eb44f297e9570f89ad30b46209e5c626e943..f19bc9635d37b87f9e468bd7a5e0076954aa12dd 100644 (file)
@@ -68,7 +68,7 @@ typedef struct _plugin_t plugin_t;
  *      success or non-zero value on error.
  *
  *   int init(int argc, char **argv, bcf_hdr_t *in_hdr, bcf_hdr_t *out_hdr)
- *      - called once at startup, allows to initialize local variables.
+ *      - called once at startup, it initializes local variables.
  *      Return 1 to suppress normal VCF/BCF header output, -1 on critical
  *      errors, 0 otherwise.
  *
@@ -552,7 +552,9 @@ static void init_data(args_t *args)
         if ( args->out_fh == NULL ) error("Can't write to \"%s\": %s\n", args->output_fname, strerror(errno));
         if ( args->n_threads ) hts_set_threads(args->out_fh, args->n_threads);
         if ( bcf_hdr_write(args->out_fh, args->hdr_out)!=0 ) error("[%s] Error: cannot write to %s\n", __func__,args->output_fname);
-        if ( args->write_index && init_index(args->out_fh,args->hdr_out,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+        if ( init_index2(args->out_fh,args->hdr_out,args->output_fname,
+                         &args->index_fn, args->write_index)<0 )
+            error("Error: failed to initialise index for %s\n",args->output_fname);
     }
 }
 
@@ -615,7 +617,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "   -l, --list-plugins             List available plugins. See BCFTOOLS_PLUGINS environment variable and man page for details\n");
     fprintf(bcftools_stderr, "   -v, --verbose                  Print verbose information, -vv increases verbosity\n");
     fprintf(bcftools_stderr, "   -V, --version                  Print version string and exit\n");
-    fprintf(bcftools_stderr, "       --write-index              Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "   -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     bcftools_exit(1);
 }
@@ -693,11 +695,11 @@ int main_plugin(int argc, char *argv[])
         {"targets-file",required_argument,NULL,'T'},
         {"targets-overlap",required_argument,NULL,2},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,10},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "h?o:O:r:R:t:T:li:e:vV",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "h?o:O:r:R:t:T:li:e:vVW::",loptions,NULL)) >= 0)
     {
         switch (c) {
             case 'V': version_only = 1; break;
@@ -742,7 +744,10 @@ int main_plugin(int argc, char *argv[])
                 break;
             case  9 : args->n_threads = strtol(optarg, 0, 0); break;
             case  8 : args->record_cmd_line = 0; break;
-            case 10 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case '?':
             case 'h': usage_only = 1; break;
             default: error("Unknown argument: %s\n", optarg);
index 5f4eb07c6ee0cfa71ba8448f8fbfe5475b98f2cc..7b1dd4391f47e2c6e22d807d6be59a743590477e 100644 (file)
@@ -55,7 +55,8 @@ typedef struct
     bcf_hdr_t *header;
     int sample_is_file;
     char **argv, *format_str, *sample_list, *targets_list, *regions_list, *vcf_list, *fn_out;
-    int argc, list_columns, print_header, allow_undef_tags, force_samples;
+    char *print_filtered;
+    int argc, list_columns, print_header, allow_undef_tags, force_samples, force_newline;
     FILE *out;
 }
 args_t;
@@ -94,9 +95,10 @@ static void init_data(args_t *args)
         smpl_ilist_destroy(ilist);
     }
     args->convert = convert_init(args->header, samples, nsamples, args->format_str);
-    convert_set_option(args->convert, force_newline, 1);
+    if ( args->force_newline ) convert_set_option(args->convert, force_newline, 1);
     convert_set_option(args->convert, subset_samples, &args->smpl_pass);
     if ( args->allow_undef_tags ) convert_set_option(args->convert, allow_undef_tags, 1);
+    if ( args->print_header>1 ) convert_set_option(args->convert, no_hdr_indices, 1);
     free(samples);
 
     int max_unpack = convert_max_unpack(args->convert);
@@ -106,6 +108,9 @@ static void init_data(args_t *args)
         max_unpack |= filter_max_unpack(args->filter);
     }
     args->files->max_unpack = max_unpack;
+    if ( !args->filter || args->print_filtered || !(filter_max_unpack(args->filter) & BCF_UN_FMT) )
+        convert_set_option(args->convert, header_samples, 1);
+    if ( args->print_filtered ) convert_set_option(args->convert, print_filtered, args->print_filtered);
 }
 
 static void destroy_data(args_t *args)
@@ -232,10 +237,12 @@ static void usage(void)
     fprintf(stderr, "Options:\n");
     fprintf(stderr, "    -e, --exclude EXPR                Exclude sites for which the expression is true (see man page for details)\n");
     fprintf(stderr, "        --force-samples               Only warn about unknown subset samples\n");
+    fprintf(stderr, "    -F, --print-filtered STR          Output STR for samples failing the -i/-e filtering expression\n");
     fprintf(stderr, "    -f, --format STRING               See man page for details\n");
-    fprintf(stderr, "    -H, --print-header                Print header\n");
+    fprintf(stderr, "    -H, --print-header                Print header, -HH to omit column indices\n");
     fprintf(stderr, "    -i, --include EXPR                Select sites for which the expression is true (see man page for details)\n");
     fprintf(stderr, "    -l, --list-samples                Print the list of samples and exit\n");
+    fprintf(stderr, "    -N, --disable-automatic-newline   Disable automatic addition of newline character when not present\n");
     fprintf(stderr, "    -o, --output FILE                 Output file name [stdout]\n");
     fprintf(stderr, "    -r, --regions REGION              Restrict to comma-separated list of regions\n");
     fprintf(stderr, "    -R, --regions-file FILE           Restrict to regions listed in a file\n");
@@ -250,6 +257,7 @@ static void usage(void)
     fprintf(stderr, "\n");
     fprintf(stderr, "Examples:\n");
     fprintf(stderr, "\tbcftools query -f '%%CHROM\\t%%POS\\t%%REF\\t%%ALT[\\t%%SAMPLE=%%GT]\\n' file.vcf.gz\n");
+    fprintf(stderr, "\t# For more examples see http://samtools.github.io/bcftools/bcftools.html#query\n");
     fprintf(stderr, "\n");
     exit(1);
 }
@@ -259,6 +267,7 @@ int main_vcfquery(int argc, char *argv[])
     int c, collapse = 0;
     args_t *args = (args_t*) calloc(1,sizeof(args_t));
     args->argc   = argc; args->argv = argv;
+    args->force_newline = 1;
     int regions_is_file = 0, targets_is_file = 0;
     int regions_overlap = 1;
     int targets_overlap = 0;
@@ -267,8 +276,10 @@ int main_vcfquery(int argc, char *argv[])
     {
         {"help",0,0,'h'},
         {"list-samples",0,0,'l'},
+        {"disable-automatic-newline",required_argument,NULL,'N'},
         {"include",1,0,'i'},
         {"exclude",1,0,'e'},
+        {"print-filtered",1,0,'F'},
         {"format",1,0,'f'},
         {"force-samples",0,0,3},
         {"output-file",1,0,'o'},
@@ -288,11 +299,13 @@ int main_vcfquery(int argc, char *argv[])
         {"allow-undef-tags",0,0,'u'},
         {0,0,0,0}
     };
-    while ((c = getopt_long(argc, argv, "hlr:R:f:a:s:S:Ht:T:c:v:i:e:o:u",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hlr:R:F:f:a:s:S:Ht:T:c:v:i:e:o:uN",loptions,NULL)) >= 0) {
         switch (c) {
             case 'o': args->fn_out = optarg; break;
+            case 'F': args->print_filtered = optarg; break;
             case 'f': args->format_str = strdup(optarg); break;
-            case 'H': args->print_header = 1; break;
+            case 'N': args->force_newline = 0; break;
+            case 'H': args->print_header++; break;
             case 'v': args->vcf_list = optarg; break;
             case 'c':
                 error("The --collapse option is obsolete, pipe through `bcftools norm -c` instead.\n");
index e4f252033dc26be96013b453950016b72440b5b5..407d2562e72d86fb8f0f2516b873e8d617873f1f 100644 (file)
@@ -57,7 +57,8 @@ typedef struct
     bcf_hdr_t *header;
     int sample_is_file;
     char **argv, *format_str, *sample_list, *targets_list, *regions_list, *vcf_list, *fn_out;
-    int argc, list_columns, print_header, allow_undef_tags, force_samples;
+    char *print_filtered;
+    int argc, list_columns, print_header, allow_undef_tags, force_samples, force_newline;
     FILE *out;
 }
 args_t;
@@ -96,9 +97,10 @@ static void init_data(args_t *args)
         smpl_ilist_destroy(ilist);
     }
     args->convert = convert_init(args->header, samples, nsamples, args->format_str);
-    convert_set_option(args->convert, force_newline, 1);
+    if ( args->force_newline ) convert_set_option(args->convert, force_newline, 1);
     convert_set_option(args->convert, subset_samples, &args->smpl_pass);
     if ( args->allow_undef_tags ) convert_set_option(args->convert, allow_undef_tags, 1);
+    if ( args->print_header>1 ) convert_set_option(args->convert, no_hdr_indices, 1);
     free(samples);
 
     int max_unpack = convert_max_unpack(args->convert);
@@ -108,6 +110,9 @@ static void init_data(args_t *args)
         max_unpack |= filter_max_unpack(args->filter);
     }
     args->files->max_unpack = max_unpack;
+    if ( !args->filter || args->print_filtered || !(filter_max_unpack(args->filter) & BCF_UN_FMT) )
+        convert_set_option(args->convert, header_samples, 1);
+    if ( args->print_filtered ) convert_set_option(args->convert, print_filtered, args->print_filtered);
 }
 
 static void destroy_data(args_t *args)
@@ -234,10 +239,12 @@ static void usage(void)
     fprintf(bcftools_stderr, "Options:\n");
     fprintf(bcftools_stderr, "    -e, --exclude EXPR                Exclude sites for which the expression is true (see man page for details)\n");
     fprintf(bcftools_stderr, "        --force-samples               Only warn about unknown subset samples\n");
+    fprintf(bcftools_stderr, "    -F, --print-filtered STR          Output STR for samples failing the -i/-e filtering expression\n");
     fprintf(bcftools_stderr, "    -f, --format STRING               See man page for details\n");
-    fprintf(bcftools_stderr, "    -H, --print-header                Print header\n");
+    fprintf(bcftools_stderr, "    -H, --print-header                Print header, -HH to omit column indices\n");
     fprintf(bcftools_stderr, "    -i, --include EXPR                Select sites for which the expression is true (see man page for details)\n");
     fprintf(bcftools_stderr, "    -l, --list-samples                Print the list of samples and exit\n");
+    fprintf(bcftools_stderr, "    -N, --disable-automatic-newline   Disable automatic addition of newline character when not present\n");
     fprintf(bcftools_stderr, "    -o, --output FILE                 Output file name [bcftools_stdout]\n");
     fprintf(bcftools_stderr, "    -r, --regions REGION              Restrict to comma-separated list of regions\n");
     fprintf(bcftools_stderr, "    -R, --regions-file FILE           Restrict to regions listed in a file\n");
@@ -252,6 +259,7 @@ static void usage(void)
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "Examples:\n");
     fprintf(bcftools_stderr, "\tbcftools query -f '%%CHROM\\t%%POS\\t%%REF\\t%%ALT[\\t%%SAMPLE=%%GT]\\n' file.vcf.gz\n");
+    fprintf(bcftools_stderr, "\t# For more examples see http://samtools.github.io/bcftools/bcftools.html#query\n");
     fprintf(bcftools_stderr, "\n");
     bcftools_exit(1);
 }
@@ -261,6 +269,7 @@ int main_vcfquery(int argc, char *argv[])
     int c, collapse = 0;
     args_t *args = (args_t*) calloc(1,sizeof(args_t));
     args->argc   = argc; args->argv = argv;
+    args->force_newline = 1;
     int regions_is_file = 0, targets_is_file = 0;
     int regions_overlap = 1;
     int targets_overlap = 0;
@@ -269,8 +278,10 @@ int main_vcfquery(int argc, char *argv[])
     {
         {"help",0,0,'h'},
         {"list-samples",0,0,'l'},
+        {"disable-automatic-newline",required_argument,NULL,'N'},
         {"include",1,0,'i'},
         {"exclude",1,0,'e'},
+        {"print-filtered",1,0,'F'},
         {"format",1,0,'f'},
         {"force-samples",0,0,3},
         {"output-file",1,0,'o'},
@@ -290,11 +301,13 @@ int main_vcfquery(int argc, char *argv[])
         {"allow-undef-tags",0,0,'u'},
         {0,0,0,0}
     };
-    while ((c = getopt_long(argc, argv, "hlr:R:f:a:s:S:Ht:T:c:v:i:e:o:u",loptions,NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "hlr:R:F:f:a:s:S:Ht:T:c:v:i:e:o:uN",loptions,NULL)) >= 0) {
         switch (c) {
             case 'o': args->fn_out = optarg; break;
+            case 'F': args->print_filtered = optarg; break;
             case 'f': args->format_str = strdup(optarg); break;
-            case 'H': args->print_header = 1; break;
+            case 'N': args->force_newline = 0; break;
+            case 'H': args->print_header++; break;
             case 'v': args->vcf_list = optarg; break;
             case 'c':
                 error("The --collapse option is obsolete, pipe through `bcftools norm -c` instead.\n");
index a0802db7e748054868543eb451229ef7e2f868c3..f1d1c86e90b8be6899ccd817a571d73714bd521a 100644 (file)
@@ -254,7 +254,7 @@ static void init_data(args_t *args)
         {
             if ( *end!=',') error("Could not parse: --buffer-size %s\n", args->buffer_size);
             args->nbuf_olap = strtol(end+1,&end,10);
-            if ( *end || args->nbuf_olap<0 ) error("Could not parse: --bufer-size %s\n", args->buffer_size);
+            if ( *end || args->nbuf_olap<0 ) error("Could not parse: --buffer-size %s\n", args->buffer_size);
         }
         if ( tmp<0 )
             args->nbuf_max = fabs(tmp)*1e6/(4+8*2)/args->roh_smpl->n;
index f9b8aab8a18329933dbac028a8130c7fefadc158..7519c6e1c3a50eb0724e7b79932793d2e0f53c34 100644 (file)
@@ -256,7 +256,7 @@ static void init_data(args_t *args)
         {
             if ( *end!=',') error("Could not parse: --buffer-size %s\n", args->buffer_size);
             args->nbuf_olap = strtol(end+1,&end,10);
-            if ( *end || args->nbuf_olap<0 ) error("Could not parse: --bufer-size %s\n", args->buffer_size);
+            if ( *end || args->nbuf_olap<0 ) error("Could not parse: --buffer-size %s\n", args->buffer_size);
         }
         if ( tmp<0 )
             args->nbuf_max = fabs(tmp)*1e6/(4+8*2)/args->roh_smpl->n;
index db01d24fd55c6e44576c90677fd2d03b71250071..f7a5dbe6e1d434e79769bc5119947821c2b7ceee 100644 (file)
@@ -37,6 +37,7 @@ THE SOFTWARE.  */
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
 #include <htslib/hts_os.h>
+#include <htslib/hts_defs.h>
 #include <inttypes.h>
 #include "bcftools.h"
 
@@ -83,10 +84,9 @@ typedef struct
 args_t;
 
 static void usage(void);
-FILE *open_file(char **fname, const char *mode, const char *fmt, ...);
-void mkdir_p(const char *fmt, ...);
+FILE *open_file(char **fname, const char *mode, const char *fmt, ...) HTS_FORMAT(HTS_PRINTF_FMT, 3, 4);
 
-char *msprintf(const char *fmt, ...)
+char * HTS_FORMAT(HTS_PRINTF_FMT, 1, 2) msprintf(const char *fmt, ...)
 {
     va_list ap;
     va_start(ap, fmt);
index effd3521066f8d5efc2238ad0107d8d63af81455..4e56158cc4cb41a456ec9e69f32c39569f6ac15a 100644 (file)
@@ -39,6 +39,7 @@ THE SOFTWARE.  */
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
 #include <htslib/hts_os.h>
+#include <htslib/hts_defs.h>
 #include <inttypes.h>
 #include "bcftools.h"
 
@@ -85,10 +86,9 @@ typedef struct
 args_t;
 
 static void usage(void);
-FILE *open_file(char **fname, const char *mode, const char *fmt, ...);
-void mkdir_p(const char *fmt, ...);
+FILE *open_file(char **fname, const char *mode, const char *fmt, ...) HTS_FORMAT(HTS_PRINTF_FMT, 3, 4);
 
-char *msprintf(const char *fmt, ...)
+char * HTS_FORMAT(HTS_PRINTF_FMT, 1, 2) msprintf(const char *fmt, ...)
 {
     va_list ap;
     va_start(ap, fmt);
index 3b208a0d3f91cb8126b08214dc9565e51390a7d9..6e21f85be8ea584db66d54d90ddbee2325d4a228 100644 (file)
@@ -1,6 +1,6 @@
 /*  vcfsort.c -- sort subcommand
 
-   Copyright (C) 2017-2023 Genome Research Ltd.
+   Copyright (C) 2017-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
 #include <htslib/vcf.h>
 #include <htslib/kstring.h>
 #include <htslib/hts_os.h>
+#include <htslib/hts_defs.h>
+#include <htslib/bgzf.h>
 #include "kheap.h"
 #include "bcftools.h"
 
+#define MAX_TMP_FILES_PER_LAYER 32
+#define MERGE_LAYERS 12
+#define MAX_TMP_FILES (MAX_TMP_FILES_PER_LAYER * MERGE_LAYERS)
+
 typedef struct
 {
     char *fname;
     htsFile *fh;
+    BGZF *bgz;
+    size_t idx;
     bcf1_t *rec;
+    int is_merged;
 }
 blk_t;
 
+typedef struct
+{
+    size_t len;
+    hts_pos_t pos;
+    int rid;
+    float qual;
+    uint8_t data[];
+}
+packed_bcf_t;
+
 typedef struct _args_t
 {
     bcf_hdr_t *hdr;
     char **argv, *fname, *output_fname, *tmp_dir;
     int argc, output_type, clevel;
     size_t max_mem, mem;
-    bcf1_t **buf;
+    packed_bcf_t **buf;
     uint8_t *mem_block;
-    size_t nbuf, mbuf, nblk;
-    blk_t *blk;
-    char *index_fn;
+
+    size_t nbuf, mbuf, nblk, tmp_count;
+    blk_t blk[MAX_TMP_FILES];
+    uint32_t tmp_layers[MERGE_LAYERS];
     int write_index;
 }
 args_t;
@@ -71,9 +91,9 @@ void clean_files(args_t *args)
 {
     int i;
     fprintf(stderr,"Cleaning\n");
-    for (i=0; i<args->nblk; i++)
+    for (i=0; i<MAX_TMP_FILES; i++)
     {
-        blk_t *blk = args->blk + i;
+        blk_t *blk = &args->blk[i];
         if ( blk->fname )
         {
             unlink(blk->fname);
@@ -84,7 +104,8 @@ void clean_files(args_t *args)
     }
     rmdir(args->tmp_dir);
 }
-void clean_files_and_throw(args_t *args, const char *format, ...)
+void HTS_FORMAT(HTS_PRINTF_FMT, 2, 3) HTS_NORETURN
+clean_files_and_throw(args_t *args, const char *format, ...)
 {
     va_list ap;
     va_start(ap, format);
@@ -95,6 +116,16 @@ void clean_files_and_throw(args_t *args, const char *format, ...)
 }
 
 int cmp_bcf_pos(const void *aptr, const void *bptr)
+{
+    bcf1_t *a = *((bcf1_t**)aptr);
+    bcf1_t *b = *((bcf1_t**)bptr);
+    if ( a->rid < b->rid ) return -1;
+    if ( a->rid > b->rid ) return 1;
+    if ( a->pos < b->pos ) return -1;
+    if ( a->pos > b->pos ) return 1;
+    return 0;
+}
+int cmp_bcf_pos_ref_alt(const void *aptr, const void *bptr)
 {
     bcf1_t *a = *((bcf1_t**)aptr);
     bcf1_t *b = *((bcf1_t**)bptr);
@@ -118,33 +149,279 @@ int cmp_bcf_pos(const void *aptr, const void *bptr)
     return 0;
 }
 
-void buf_flush(args_t *args)
+static int cmp_packed_bcf_pos_ref_alt(const void *aptr, const void *bptr)
 {
-    if ( !args->nbuf ) return;
+    packed_bcf_t *a = *(packed_bcf_t **) aptr;
+    packed_bcf_t *b = *(packed_bcf_t **) bptr;
 
-    qsort(args->buf, args->nbuf, sizeof(*args->buf), cmp_bcf_pos);
+    if ( a->rid < b->rid ) return -1;
+    if ( a->rid > b->rid ) return 1;
+    if ( a->pos < b->pos ) return -1;
+    if ( a->pos > b->pos ) return 1;
+    
+    // Sort lexicographically by ref,alt.  These are stored tab-separated
+    // as the first item in packed_bcf_t::data
+    return strcmp((char *) a->data, (char *) b->data);
+}
 
-    args->nblk++;
-    args->blk = (blk_t*) realloc(args->blk, sizeof(blk_t)*args->nblk);
-    if ( !args->blk ) error("Error: could not allocate %zu bytes of memory, try reducing --max-mem\n",sizeof(blk_t)*args->nblk);
-    blk_t *blk = args->blk + args->nblk - 1;
+static int cmp_packed_bcf_pos_ref_alt_stable(const void *aptr, const void *bptr)
+{
+    // cmp_bcf_pos_ref_alt() with tie-breaker to make qsort stable
+    int res = cmp_packed_bcf_pos_ref_alt(aptr, bptr);
+    if (res != 0) return res;
+
+    // Got a tie - use the position in the original input to break it.
+    // As everything is read into a big memory buffer, for most records
+    // we can just compare the pointers directly.  The exception is
+    // any record that didn't quite fit in the memory buffer, causing it to be
+    // flushed.  Those are flagged by setting packed_bcf_t::len = SIZE_MAX, and
+    // as they were the last record in the segment, they should always sort
+    // after unflagged records.
+
+    packed_bcf_t *a = *(packed_bcf_t **) aptr;
+    packed_bcf_t *b = *(packed_bcf_t **) bptr;
+
+    if (a->len == SIZE_MAX) return 1;
+    if (b->len == SIZE_MAX) return -1;
+
+    return a < b ? -1 : 1;
+}
+
+static uint8_t *pack_unsigned(uint8_t *data, uint64_t val)
+{
+    do {
+        *data++ = (val & 0x7f) | ((val > 0x7f) ? 0x80 : 0);
+        val >>= 7;
+    } while (val > 0);
+    return data;    
+}
+
+static uint8_t *pack_hts_pos(uint8_t *data, hts_pos_t val)
+{
+    uint64_t sign = val < 0;
+    uint64_t v = val < 0 ? -(val + 1) : val;
+    v = v << 1 | sign;
+    return pack_unsigned(data, v);
+}
+
+static uint8_t *pack_bcf_data(packed_bcf_t *dest, const bcf1_t *src,
+                              int outside_buffer)
+{
+    uint32_t i;
+    uint8_t *data = dest->data;
+    uint8_t *start = dest->data;
+    dest->pos = src->pos;
+    dest->rid = src->rid;
+    dest->qual = src->qual;
+
+    // Copy in alleles, for the comparison function
+    for (i = 0; i < src->n_allele; i++)
+    {
+        size_t l = strlen(src->d.allele[i]);
+        if (i > 0) *data++ = '\t';
+        memcpy(data, src->d.allele[i], l);
+        data += l;
+    }
+    *data++ = '\0';
+
+    if (outside_buffer)
+    {
+        dest->len = SIZE_MAX;
+        memcpy(data, &src, sizeof(src));
+        data += sizeof(src);
+        return data;
+    }
+
+    data = pack_hts_pos(data, src->rlen);
+    data = pack_unsigned(data, src->n_info);
+    data = pack_unsigned(data, src->n_allele);
+    data = pack_unsigned(data, src->n_fmt);
+    data = pack_unsigned(data, src->n_sample);
+    data = pack_unsigned(data, src->shared.l);
+    data = pack_unsigned(data, src->indiv.l);
+    if (src->shared.l)
+        memcpy(data, src->shared.s, src->shared.l);
+    data += src->shared.l;
+    if (src->indiv.l)
+        memcpy(data, src->indiv.s, src->indiv.l);
+    data += src->indiv.l;
+    dest->len = data - start;
+    return data;
+}
+
+static int write_packed_bcf(BGZF *fp, packed_bcf_t *src)
+{
+    // Write pos, rid, qual
+    size_t len = src->data - (uint8_t *) &src->pos;
+    if (bgzf_write_small(fp, &src->pos, len) < len)
+        return -1;
+
+    // Skip the copy of the alleles
+    size_t skip = strlen((char *) src->data) + 1;
+ 
+    // Write everything else
+    if (src->len < SIZE_MAX)
+    {
+        // In main memory block
+        len = src->len - skip;
+        if (bgzf_write_small(fp, src->data + skip, len) < len)
+            return -1;
+    }
+    else
+    {
+        // Record didn't fit in the main block.  To minimize the
+        // overflow, its packed_bcf_t data will be imcomplete.  A pointer to
+        // its bcf1_t struct will have been placed after the allele data
+        // so we can finish the packing job and write it in the same format
+        // the rest of the data
+        bcf1_t *rec;
+        uint8_t tmp[100], *data = tmp;
+        memcpy(&rec, src->data + skip, sizeof(rec));
+
+        data = pack_hts_pos(data, rec->rlen);
+        data = pack_unsigned(data, rec->n_info);
+        data = pack_unsigned(data, rec->n_allele);
+        data = pack_unsigned(data, rec->n_fmt);
+        data = pack_unsigned(data, rec->n_sample);
+        data = pack_unsigned(data, rec->shared.l);
+        data = pack_unsigned(data, rec->indiv.l);
+        if (bgzf_write_small(fp, tmp, data - tmp) < data - tmp)
+            return -1;
+        if (rec->shared.l > 0 &&
+            bgzf_write_small(fp, rec->shared.s, rec->shared.l) < rec->shared.l)
+            return -1;
+        if (rec->indiv.l > 0 &&
+            bgzf_write_small(fp, rec->indiv.s, rec->indiv.l) < rec->indiv.l)
+            return -1;
+    }
+
+    return 0;
+}
+
+static uint64_t unpack_unsigned(BGZF *fp, int *err)
+{
+    uint8_t data;
+    uint64_t val = 0;
+    uint32_t i = 0;
+
+    if (bgzf_read_small(fp, &data, sizeof(data)) <= 0)
+        goto short_read;
+
+    while (data & 0x80)
+    {
+        val |= (uint64_t)(data & 0x7f) << i;
+        i += 7;
+        if (bgzf_read_small(fp, &data, sizeof(data)) <= 0)
+            goto short_read;
+    }
+    val |= (uint64_t)data << i;
+    return val;
+
+ short_read:
+    *err = 1;
+    return 0;
+}
+
+static hts_pos_t unpack_hts_pos(BGZF *fp, int *err)
+{
+    uint64_t v = unpack_unsigned(fp, err);
+
+    if ((v & 1) == 0)
+        return (hts_pos_t)(v >> 1);
+    else
+        return -(hts_pos_t)(v >> 1) - 1;
+}
+
+static int read_packed_bcf(BGZF *fp, bcf1_t *dest)
+{
+    int err = 0;
+    packed_bcf_t tmp;
+    size_t len = tmp.data - (uint8_t *) &tmp.pos;
+    
+    bcf_clear(dest);
+    ssize_t got = bgzf_read_small(fp, &tmp.pos, len);
+    if (got == 0)
+        return -1;  // EOF
+    if (got < len)
+        return -2;  // Error or short read
+    dest->pos = tmp.pos;
+    dest->rid = tmp.rid;
+    dest->qual = tmp.qual;
+    dest->rlen = unpack_hts_pos(fp, &err);
+    dest->n_info = unpack_unsigned(fp, &err);
+    dest->n_allele = unpack_unsigned(fp, &err);
+    dest->n_fmt = unpack_unsigned(fp, &err);
+    dest->n_sample = unpack_unsigned(fp, &err);
+    len = unpack_unsigned(fp, &err);
+    if (ks_resize(&dest->shared, len) != 0)
+        return -2;
+    dest->shared.l = len;
+    len = unpack_unsigned(fp, &err);
+    if (ks_resize(&dest->indiv, len) != 0)
+        return -2;
+    dest->indiv.l = len;
+    err |= bgzf_read_small(fp, dest->shared.s, dest->shared.l) < dest->shared.l;
+    err |= bgzf_read_small(fp, dest->indiv.s, dest->indiv.l) < dest->indiv.l;
+    return err == 0 ? 0 : -2;
+}
 
+void open_tmp_file(args_t *args, blk_t *blk, int is_merged)
+{
     kstring_t str = {0,0,0};
-    ksprintf(&str, "%s/%05d.bcf", args->tmp_dir, (int)args->nblk);
-    blk->fname = str.s;
-    blk->rec   = NULL;
-    blk->fh    = NULL;
+    int tries = 1000;
 
-    htsFile *fh = hts_open(blk->fname, "wbu");
-    if ( fh == NULL ) clean_files_and_throw(args, "Cannot write %s: %s\n", blk->fname, strerror(errno));
-    if ( bcf_hdr_write(fh, args->hdr)!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__,blk->fname);
+    blk->fh = NULL;
+    blk->bgz = NULL;
+
+    do {
+        if (ksprintf(ks_clear(&str), "%s/%05zd%s",
+                     args->tmp_dir, args->tmp_count++,
+                     is_merged ? ".bcf" : "") < 0) {
+            clean_files_and_throw(args, "%s", strerror(errno));
+        }
+
+        if (is_merged)
+            blk->fh = hts_open(str.s, "wbx1");
+        else
+            blk->bgz = bgzf_open(str.s, "wx1");
+        if ( blk->fh == NULL && blk->bgz == NULL && (errno != EEXIST || --tries <= 0)) {
+            clean_files_and_throw(args, "Cannot write %s: %s\n",
+                                  str.s, strerror(errno));
+        }
+    } while (blk->fh == NULL && blk->bgz == NULL);
+
+    blk->fname = ks_release(&str);
+    blk->idx = args->tmp_count - 1;
+}
+
+void do_partial_merge(args_t *args);
+
+void buf_flush(args_t *args, bcf1_t *last_rec)
+{
+    if ( !args->nbuf ) return;
+
+    qsort(args->buf, args->nbuf, sizeof(*args->buf), cmp_packed_bcf_pos_ref_alt_stable);
+
+    if (args->tmp_layers[0] >= MAX_TMP_FILES_PER_LAYER)
+        do_partial_merge(args);
+
+    assert(args->nblk < MAX_TMP_FILES);
+    blk_t *blk = &args->blk[args->nblk];
+    blk->is_merged = 0;
+    args->nblk++;
+    args->tmp_layers[0]++;
 
+    assert(blk->fname == NULL && blk->fh == NULL && blk->bgz == NULL);
+
+    open_tmp_file(args, blk, 0);
     int i;
     for (i=0; i<args->nbuf; i++)
     {
-        if ( bcf_write(fh, args->hdr, args->buf[i])!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__,blk->fname);
+        if ( write_packed_bcf(blk->bgz, args->buf[i])!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__,blk->fname);
     }
-    if ( hts_close(fh)!=0 ) clean_files_and_throw(args, "[%s] Error: close failed .. %s\n", __func__,blk->fname);
+
+    if ( bgzf_close(blk->bgz)!=0 ) clean_files_and_throw(args, "[%s] Error: close failed .. %s\n", __func__,blk->fname);
+    blk->bgz = NULL;
 
     args->nbuf = 0;
     args->mem  = 0;
@@ -156,19 +433,38 @@ static inline uint8_t *_align_up(uint8_t *ptr)
     return (uint8_t*)(((size_t)ptr + 8 - 1) & ~((size_t)(8 - 1)));
 }
 
+#define varint_size(X) ((sizeof(X) * 8 + 7) / 7) // worst case
+
 void buf_push(args_t *args, bcf1_t *rec)
 {
-    size_t delta = sizeof(bcf1_t) + rec->shared.l + rec->indiv.l + rec->unpack_size[0] + rec->unpack_size[1]
-        + sizeof(*rec->d.allele)*rec->d.m_allele
-        + sizeof(bcf1_t*)       // args->buf
+    size_t delta = sizeof(rec->pos)
+        + sizeof(rec->rid)
+        + sizeof(rec->qual)
+        + varint_size(rec->rlen)
+        + varint_size(2) // n_info
+        + varint_size(2) // n_allele
+        + varint_size(1) // n_fmt
+        + varint_size(3) // n_sample
+        + varint_size(rec->shared.l)
+        + varint_size(rec->indiv.l)
+        + rec->shared.l + rec->indiv.l
+        + rec->unpack_size[1]   // Alleles
         + 8;                    // the number of _align_up() calls
 
     if ( delta > args->max_mem - args->mem )
     {
+        packed_bcf_t *tmp = malloc(sizeof(*tmp) + rec->unpack_size[1] * sizeof(bcf1_t *));
+        if (!tmp)
+            clean_files_and_throw(args, "[%s] Out of memory\n", __func__);
+        pack_bcf_data(tmp, rec, 1);
+
         args->nbuf++;
         hts_expand(bcf1_t*, args->nbuf, args->mbuf, args->buf);
-        args->buf[args->nbuf-1] = rec;
-        buf_flush(args);
+        args->buf[args->nbuf-1] = tmp;
+
+        buf_flush(args, rec);
+
+        free(tmp);
         bcf_destroy(rec);
         return;
     }
@@ -178,48 +474,13 @@ void buf_push(args_t *args, bcf1_t *rec)
 
     uint8_t *ptr_beg = args->mem_block + args->mem;
     uint8_t *ptr = _align_up(ptr_beg);
-    bcf1_t *new_rec = (bcf1_t*)ptr;
-    memcpy(new_rec,rec,sizeof(*rec));
-    ptr += sizeof(*rec);
-
-    // The array of allele pointers does not need alignment as bcf1_t is already padded to the biggest
-    // data type in the structure
-    char **allele = (char**)ptr;
-    ptr += rec->n_allele*sizeof(*allele);
-
-    // This is just to prevent valgrind from complaining about memcpy, unpack_size is a high-water mark
-    // and the end may be uninitialized
-    delta = rec->d.allele[rec->n_allele-1] - rec->d.allele[0];
-    while ( delta < rec->unpack_size[1] ) if ( !rec->d.als[delta++] ) break;
-    memcpy(ptr,rec->d.als,delta);
-    new_rec->d.als = (char*)ptr;
-    ptr = ptr + delta;
+    packed_bcf_t *packed_rec = (packed_bcf_t *) ptr;
 
-    int i;
-    for (i=0; i<rec->n_allele; i++) allele[i] = new_rec->d.als + (ptrdiff_t)(rec->d.allele[i] - rec->d.allele[0]);
-    new_rec->d.allele = allele;
-
-    memcpy(ptr,rec->shared.s,rec->shared.l);
-    new_rec->shared.s = (char*)ptr;
-    new_rec->shared.m = rec->shared.l;
-    ptr += rec->shared.l;
-
-    memcpy(ptr,rec->indiv.s,rec->indiv.l);
-    new_rec->indiv.s = (char*)ptr;
-    new_rec->indiv.m = rec->indiv.l;
-    ptr += rec->indiv.l;
-
-    // This is just to prevent valgrind from complaining about memcpy, unpack_size is a high-water mark
-    // and the end may be uninitialized
-    i = 0;
-    while ( i < rec->unpack_size[0] ) if ( !rec->d.id[i++] ) break;
-    memcpy(ptr,rec->d.id,i);
-    new_rec->d.id = (char*)ptr;
-    ptr += i;
+    ptr = pack_bcf_data(packed_rec, rec, 0);
 
     args->nbuf++;
     hts_expand(bcf1_t*, args->nbuf, args->mbuf, args->buf);
-    args->buf[args->nbuf-1] = new_rec;
+    args->buf[args->nbuf-1] = packed_rec;
 
     delta = ptr - ptr_beg;
     args->mem += delta;
@@ -246,11 +507,11 @@ void sort_blocks(args_t *args)
             bcf_destroy(rec);
             break;
         }
-        if ( rec->errcode ) clean_files_and_throw(args,"Error encountered while parsing the input at %s:%d\n",bcf_seqname(args->hdr,rec),rec->pos+1);
+        if ( rec->errcode ) clean_files_and_throw(args,"Error encountered while parsing the input at %s:%"PRIhts_pos"\n",bcf_seqname(args->hdr,rec),rec->pos+1);
         bcf_unpack(rec, BCF_UN_STR);
         buf_push(args, rec);
     }
-    buf_flush(args);
+    buf_flush(args, NULL);
     free(args->buf);
 
     if ( hts_close(in)!=0 ) clean_files_and_throw(args,"Close failed: %s\n", args->fname);
@@ -260,49 +521,80 @@ static inline int blk_is_smaller(blk_t **aptr, blk_t **bptr)
 {
     blk_t *a = *aptr;
     blk_t *b = *bptr;
-    int ret = cmp_bcf_pos(&a->rec, &b->rec);
+    int ret = cmp_bcf_pos_ref_alt(&a->rec, &b->rec);
     if ( ret < 0 ) return 1;
+    if (ret == 0 && a->idx < b->idx) return 1;
     return 0;
 }
 KHEAP_INIT(blk, blk_t*, blk_is_smaller)
 
 void blk_read(args_t *args, khp_blk_t *bhp, bcf_hdr_t *hdr, blk_t *blk)
 {
-    if ( !blk->fh ) return;
-    int ret = bcf_read(blk->fh, hdr, blk->rec);
+    int ret;
+    if (blk->is_merged)
+    {
+        if ( !blk->fh ) return;
+        ret = bcf_read(blk->fh, hdr, blk->rec);
+    }
+    else
+    {
+        if ( !blk->bgz ) return;
+        ret = read_packed_bcf(blk->bgz, blk->rec);
+    }
     if ( ret < -1 ) clean_files_and_throw(args, "Error reading %s\n", blk->fname);
     if ( ret == -1 )
     {
-        if ( hts_close(blk->fh)!=0 ) clean_files_and_throw(args, "Close failed: %s\n", blk->fname);
-        blk->fh = 0;
-        return;
+        if (blk->is_merged)
+        {
+            if ( hts_close(blk->fh)!=0 ) clean_files_and_throw(args, "Close failed: %s\n", blk->fname);
+            blk->fh = NULL;
+            return;
+        }
+        else
+        {
+            if ( bgzf_close(blk->bgz) != 0)
+                clean_files_and_throw(args, "Close failed: %s\n", blk->fname);
+            blk->bgz = NULL;
+            return;
+        }
     }
     bcf_unpack(blk->rec, BCF_UN_STR);
     khp_insert(blk, bhp, &blk);
 }
 
-void merge_blocks(args_t *args)
+void merge_blocks(args_t *args, htsFile *out, const char *output_fname,
+                  int idx_fmt, size_t from)
 {
-    fprintf(stderr,"Merging %d temporary files\n", (int)args->nblk);
     khp_blk_t *bhp = khp_init(blk);
+    char *index_fn = NULL;
+    size_t i;
 
-    int i;
-    for (i=0; i<args->nblk; i++)
+    for (i=from; i<args->nblk; i++)
     {
-        blk_t *blk = args->blk + i;
-        blk->fh = hts_open(blk->fname, "r");
-        if ( !blk->fh ) clean_files_and_throw(args, "Could not read %s: %s\n", blk->fname, strerror(errno));
-        bcf_hdr_t *hdr = bcf_hdr_read(blk->fh);
-        bcf_hdr_destroy(hdr);
-        blk->rec = bcf_init();
+        blk_t *blk = &args->blk[i];
+        if (blk->is_merged)
+        {
+            blk->fh = hts_open(blk->fname, "r");
+            if ( !blk->fh ) clean_files_and_throw(args, "Could not read %s: %s\n", blk->fname, strerror(errno));
+            bcf_hdr_t *hdr = bcf_hdr_read(blk->fh);
+            bcf_hdr_destroy(hdr);
+        }
+        else
+        {
+            blk->bgz = bgzf_open(blk->fname, "r");
+            if (!blk->bgz)
+                clean_files_and_throw(args, "Could not read %s: %s\n", blk->fname, strerror(errno));
+        }
         blk_read(args, bhp, args->hdr, blk);
     }
 
-    char wmode[8];
-    set_wmode(wmode,args->output_type,args->output_fname,args->clevel);
-    htsFile *out = hts_open(args->output_fname ? args->output_fname : "-", wmode);
-    if ( bcf_hdr_write(out, args->hdr)!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(out,args->hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( bcf_hdr_write(out, args->hdr)!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__, output_fname);
+
+    if (idx_fmt) {
+        if ( init_index2(out,args->hdr,output_fname,&index_fn,idx_fmt)<0 )
+            error("Error: failed to initialise index for %s\n",output_fname);
+    }
+
     while ( bhp->ndat )
     {
         blk_t *blk = bhp->dat[0];
@@ -310,22 +602,93 @@ void merge_blocks(args_t *args)
         khp_delete(blk, bhp);
         blk_read(args, bhp, args->hdr, blk);
     }
-    if ( args->write_index )
+    if ( idx_fmt )
     {
         if ( bcf_idx_save(out)<0 )
         {
-            if ( hts_close(out)!=0 ) error("Error: close failed .. %s\n", args->output_fname?args->output_fname:"stdout");
-            error("Error: cannot write to index %s\n", args->index_fn);
+            if ( hts_close(out)!=0 ) error("Error: close failed .. %s\n", output_fname);
+            error("Error: cannot write to index %s\n", index_fn);
         }
-        free(args->index_fn);
+        free(index_fn);
     }
-    if ( hts_close(out)!=0 ) clean_files_and_throw(args, "Close failed: %s\n", args->output_fname);
 
-    clean_files(args);
+    for (i = from; i < args->nblk; i++)
+    {
+        blk_t *blk = &args->blk[i];
+        if (unlink(blk->fname) != 0)
+            clean_files_and_throw(args, "Couldn't remove temporary file %s\n", blk->fname);
+        free(blk->fname);
+        blk->fname = NULL;
+    }
 
-    free(args->blk);
     khp_destroy(blk, bhp);
+}
+
+void do_partial_merge(args_t *args)
+{
+    uint32_t to_layer = 0;
+    size_t to_merge = 0;
+
+    // Temp. files are arranged in layers of at most MAX_TMP_FILES_PER_LAYER.
+    // When a layer is full, it is merged into the next layer up.  Each
+    // layer will therefore contain files with exponentially more records
+    // then the previous one, but will be merged exponentially less frequently.
+    // The result is that the overall complexity will remain O(n*log(n))
+    // even if we need to do lots of partial merges.
+
+    while (to_layer < MERGE_LAYERS
+           && args->tmp_layers[to_layer] >= MAX_TMP_FILES_PER_LAYER)
+    {
+        to_merge += args->tmp_layers[to_layer];
+        args->tmp_layers[to_layer] = 0;
+        to_layer++;
+    }
+
+    assert(to_merge > 0 && to_merge <= args->nblk);
+
+    if (to_layer == MERGE_LAYERS) {
+        // Edge case - if we've got here, we've completely used the
+        // temp file allocation, so merge absolutely everything and
+        // leave one file at the highest level.  Strictly this breaks
+        // the O(n*log(n)) complexity, but unless MERGE_LAYERS and
+        // MAX_TMP_FILES_PER_LAYER are too small it would take so long
+        // to get here it should never actually happen...
+        assert(to_merge == MAX_TMP_FILES_PER_LAYER * MERGE_LAYERS);
+        to_layer = MERGE_LAYERS - 1;
+    }
+
+    blk_t tmp = { NULL };
+    open_tmp_file(args, &tmp, 1);
+    merge_blocks(args, tmp.fh, tmp.fname, 0, args->nblk - to_merge);
+    if (hts_close(tmp.fh) != 0)
+        clean_files_and_throw(args, "Close failed: %s\n", tmp.fname);
+
+    args->nblk -= to_merge;
+    assert(args->blk[args->nblk].fh == NULL);
+    assert(args->blk[args->nblk].fname == NULL);
+    args->blk[args->nblk].is_merged = 1;
+    args->blk[args->nblk].idx = tmp.idx;
+    args->blk[args->nblk++].fname = tmp.fname;
+    args->tmp_layers[to_layer]++;
+}
+
+void merge_to_output(args_t *args)
+{
+    char wmode[8] = { 0 };
+    set_wmode(wmode,args->output_type,args->output_fname,args->clevel);
+    const char *output_fname = args->output_fname ? args->output_fname : "-";
+
+    htsFile *out = hts_open(output_fname, wmode);
+    if (!out) clean_files_and_throw(args, "[%s] Error: cannot open %s\n", __func__, output_fname);
+
+    fprintf(stderr,"Merging %zd temporary files\n", args->nblk);
+    merge_blocks(args, out, output_fname, args->write_index, 0);
     fprintf(stderr,"Done\n");
+
+    if ( hts_close(out)!=0 )
+        clean_files_and_throw(args, "Close failed: %s\n", output_fname);
+
+    clean_files(args);
 }
 
 static void usage(args_t *args)
@@ -337,7 +700,6 @@ static void usage(args_t *args)
     fprintf(stderr, "Options:\n");
     fprintf(stderr, "    -m, --max-mem FLOAT[kMG]       maximum memory to use [768M]\n");    // using metric units, 1M=1e6
     fprintf(stderr, "    -o, --output FILE              output file name [stdout]\n");
-    fprintf(stderr, "    -O, --output-type b|u|z|v      b: compressed BCF, u: uncompressed BCF, z: compressed VCF, v: uncompressed VCF [v]\n");
     fprintf(stderr, "    -O, --output-type u|b|v|z[0-9] u/b: un/compressed BCF, v/z: un/compressed VCF, 0-9: compression level [v]\n");
 
 #ifdef _WIN32
@@ -345,7 +707,7 @@ static void usage(args_t *args)
 #else
     fprintf(stderr, "    -T, --temp-dir DIR             temporary files [/tmp/bcftools.XXXXXX]\n");
 #endif
-    fprintf(stderr, "        --write-index              Automatically index the output files [off]\n");
+    fprintf(stderr, "    -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     exit(1);
 }
@@ -361,14 +723,23 @@ size_t parse_mem_string(const char *str)
     return mem;
 }
 
-void mkdir_p(const char *fmt, ...);
 static void init(args_t *args)
 {
+    size_t i;
     args->max_mem *= 0.9;
     args->mem_block = malloc(args->max_mem);
     if ( !args->mem_block ) error("Error: could not allocate %zu bytes of memory, try reducing --max-mem\n",args->max_mem);
     args->mem = 0;
 
+    for (i = 0; i < MAX_TMP_FILES; i++)
+    {
+        args->blk[i].fname = NULL;
+        args->blk[i].rec = bcf_init();
+        if (!args->blk[i].rec)
+            clean_files_and_throw(args,"Couldn't allocate bcf record\n");
+    }
+
+
     args->tmp_dir = init_tmp_prefix(args->tmp_dir);
 
 #ifdef _WIN32
@@ -408,11 +779,11 @@ int main_sort(int argc, char *argv[])
         {"output-file",required_argument,NULL,'o'},
         {"output",required_argument,NULL,'o'},
         {"help",no_argument,NULL,'h'},
-        {"write-index",no_argument,NULL,1},
+        {"write-index",optional_argument,NULL,'W'},
         {0,0,0,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "m:T:O:o:h?",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "m:T:O:o:W::h?",loptions,NULL)) >= 0)
     {
         switch (c)
         {
@@ -437,7 +808,10 @@ int main_sort(int argc, char *argv[])
                           if ( *tmp || args->clevel<0 || args->clevel>9 ) error("Could not parse argument: --compression-level %s\n", optarg+1);
                       }
                       break;
-            case  1 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'h':
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
@@ -453,7 +827,7 @@ int main_sort(int argc, char *argv[])
 
     init(args);
     sort_blocks(args);
-    merge_blocks(args);
+    merge_to_output(args);
     destroy(args);
 
     return 0;
index 948d60b777eae845b65293d3de1a792fbcca843d..da899ae765c426fcd4bbe7c35192796a3095fdb9 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  vcfsort.c -- sort subcommand
 
-   Copyright (C) 2017-2023 Genome Research Ltd.
+   Copyright (C) 2017-2024 Genome Research Ltd.
 
    Author: Petr Danecek <pd3@sanger.ac.uk>
 
 #include <htslib/vcf.h>
 #include <htslib/kstring.h>
 #include <htslib/hts_os.h>
+#include <htslib/hts_defs.h>
+#include <htslib/bgzf.h>
 #include "kheap.h"
 #include "bcftools.h"
 
+#define MAX_TMP_FILES_PER_LAYER 32
+#define MERGE_LAYERS 12
+#define MAX_TMP_FILES (MAX_TMP_FILES_PER_LAYER * MERGE_LAYERS)
+
 typedef struct
 {
     char *fname;
     htsFile *fh;
+    BGZF *bgz;
+    size_t idx;
     bcf1_t *rec;
+    int is_merged;
 }
 blk_t;
 
+typedef struct
+{
+    size_t len;
+    hts_pos_t pos;
+    int rid;
+    float qual;
+    uint8_t data[];
+}
+packed_bcf_t;
+
 typedef struct _args_t
 {
     bcf_hdr_t *hdr;
     char **argv, *fname, *output_fname, *tmp_dir;
     int argc, output_type, clevel;
     size_t max_mem, mem;
-    bcf1_t **buf;
+    packed_bcf_t **buf;
     uint8_t *mem_block;
-    size_t nbuf, mbuf, nblk;
-    blk_t *blk;
-    char *index_fn;
+
+    size_t nbuf, mbuf, nblk, tmp_count;
+    blk_t blk[MAX_TMP_FILES];
+    uint32_t tmp_layers[MERGE_LAYERS];
     int write_index;
 }
 args_t;
@@ -73,9 +93,9 @@ void clean_files(args_t *args)
 {
     int i;
     fprintf(bcftools_stderr,"Cleaning\n");
-    for (i=0; i<args->nblk; i++)
+    for (i=0; i<MAX_TMP_FILES; i++)
     {
-        blk_t *blk = args->blk + i;
+        blk_t *blk = &args->blk[i];
         if ( blk->fname )
         {
             unlink(blk->fname);
@@ -86,7 +106,8 @@ void clean_files(args_t *args)
     }
     rmdir(args->tmp_dir);
 }
-void clean_files_and_throw(args_t *args, const char *format, ...)
+void HTS_FORMAT(HTS_PRINTF_FMT, 2, 3) HTS_NORETURN
+clean_files_and_throw(args_t *args, const char *format, ...)
 {
     va_list ap;
     va_start(ap, format);
@@ -97,6 +118,16 @@ void clean_files_and_throw(args_t *args, const char *format, ...)
 }
 
 int cmp_bcf_pos(const void *aptr, const void *bptr)
+{
+    bcf1_t *a = *((bcf1_t**)aptr);
+    bcf1_t *b = *((bcf1_t**)bptr);
+    if ( a->rid < b->rid ) return -1;
+    if ( a->rid > b->rid ) return 1;
+    if ( a->pos < b->pos ) return -1;
+    if ( a->pos > b->pos ) return 1;
+    return 0;
+}
+int cmp_bcf_pos_ref_alt(const void *aptr, const void *bptr)
 {
     bcf1_t *a = *((bcf1_t**)aptr);
     bcf1_t *b = *((bcf1_t**)bptr);
@@ -120,33 +151,279 @@ int cmp_bcf_pos(const void *aptr, const void *bptr)
     return 0;
 }
 
-void buf_flush(args_t *args)
+static int cmp_packed_bcf_pos_ref_alt(const void *aptr, const void *bptr)
 {
-    if ( !args->nbuf ) return;
+    packed_bcf_t *a = *(packed_bcf_t **) aptr;
+    packed_bcf_t *b = *(packed_bcf_t **) bptr;
 
-    qsort(args->buf, args->nbuf, sizeof(*args->buf), cmp_bcf_pos);
+    if ( a->rid < b->rid ) return -1;
+    if ( a->rid > b->rid ) return 1;
+    if ( a->pos < b->pos ) return -1;
+    if ( a->pos > b->pos ) return 1;
+    
+    // Sort lexicographically by ref,alt.  These are stored tab-separated
+    // as the first item in packed_bcf_t::data
+    return strcmp((char *) a->data, (char *) b->data);
+}
 
-    args->nblk++;
-    args->blk = (blk_t*) realloc(args->blk, sizeof(blk_t)*args->nblk);
-    if ( !args->blk ) error("Error: could not allocate %zu bytes of memory, try reducing --max-mem\n",sizeof(blk_t)*args->nblk);
-    blk_t *blk = args->blk + args->nblk - 1;
+static int cmp_packed_bcf_pos_ref_alt_stable(const void *aptr, const void *bptr)
+{
+    // cmp_bcf_pos_ref_alt() with tie-breaker to make qsort stable
+    int res = cmp_packed_bcf_pos_ref_alt(aptr, bptr);
+    if (res != 0) return res;
+
+    // Got a tie - use the position in the original input to break it.
+    // As everything is read into a big memory buffer, for most records
+    // we can just compare the pointers directly.  The exception is
+    // any record that didn't quite fit in the memory buffer, causing it to be
+    // flushed.  Those are flagged by setting packed_bcf_t::len = SIZE_MAX, and
+    // as they were the last record in the segment, they should always sort
+    // after unflagged records.
+
+    packed_bcf_t *a = *(packed_bcf_t **) aptr;
+    packed_bcf_t *b = *(packed_bcf_t **) bptr;
+
+    if (a->len == SIZE_MAX) return 1;
+    if (b->len == SIZE_MAX) return -1;
+
+    return a < b ? -1 : 1;
+}
+
+static uint8_t *pack_unsigned(uint8_t *data, uint64_t val)
+{
+    do {
+        *data++ = (val & 0x7f) | ((val > 0x7f) ? 0x80 : 0);
+        val >>= 7;
+    } while (val > 0);
+    return data;    
+}
+
+static uint8_t *pack_hts_pos(uint8_t *data, hts_pos_t val)
+{
+    uint64_t sign = val < 0;
+    uint64_t v = val < 0 ? -(val + 1) : val;
+    v = v << 1 | sign;
+    return pack_unsigned(data, v);
+}
+
+static uint8_t *pack_bcf_data(packed_bcf_t *dest, const bcf1_t *src,
+                              int outside_buffer)
+{
+    uint32_t i;
+    uint8_t *data = dest->data;
+    uint8_t *start = dest->data;
+    dest->pos = src->pos;
+    dest->rid = src->rid;
+    dest->qual = src->qual;
+
+    // Copy in alleles, for the comparison function
+    for (i = 0; i < src->n_allele; i++)
+    {
+        size_t l = strlen(src->d.allele[i]);
+        if (i > 0) *data++ = '\t';
+        memcpy(data, src->d.allele[i], l);
+        data += l;
+    }
+    *data++ = '\0';
+
+    if (outside_buffer)
+    {
+        dest->len = SIZE_MAX;
+        memcpy(data, &src, sizeof(src));
+        data += sizeof(src);
+        return data;
+    }
+
+    data = pack_hts_pos(data, src->rlen);
+    data = pack_unsigned(data, src->n_info);
+    data = pack_unsigned(data, src->n_allele);
+    data = pack_unsigned(data, src->n_fmt);
+    data = pack_unsigned(data, src->n_sample);
+    data = pack_unsigned(data, src->shared.l);
+    data = pack_unsigned(data, src->indiv.l);
+    if (src->shared.l)
+        memcpy(data, src->shared.s, src->shared.l);
+    data += src->shared.l;
+    if (src->indiv.l)
+        memcpy(data, src->indiv.s, src->indiv.l);
+    data += src->indiv.l;
+    dest->len = data - start;
+    return data;
+}
+
+static int write_packed_bcf(BGZF *fp, packed_bcf_t *src)
+{
+    // Write pos, rid, qual
+    size_t len = src->data - (uint8_t *) &src->pos;
+    if (bgzf_write_small(fp, &src->pos, len) < len)
+        return -1;
+
+    // Skip the copy of the alleles
+    size_t skip = strlen((char *) src->data) + 1;
+ 
+    // Write everything else
+    if (src->len < SIZE_MAX)
+    {
+        // In main memory block
+        len = src->len - skip;
+        if (bgzf_write_small(fp, src->data + skip, len) < len)
+            return -1;
+    }
+    else
+    {
+        // Record didn't fit in the main block.  To minimize the
+        // overflow, its packed_bcf_t data will be imcomplete.  A pointer to
+        // its bcf1_t struct will have been placed after the allele data
+        // so we can finish the packing job and write it in the same format
+        // the rest of the data
+        bcf1_t *rec;
+        uint8_t tmp[100], *data = tmp;
+        memcpy(&rec, src->data + skip, sizeof(rec));
+
+        data = pack_hts_pos(data, rec->rlen);
+        data = pack_unsigned(data, rec->n_info);
+        data = pack_unsigned(data, rec->n_allele);
+        data = pack_unsigned(data, rec->n_fmt);
+        data = pack_unsigned(data, rec->n_sample);
+        data = pack_unsigned(data, rec->shared.l);
+        data = pack_unsigned(data, rec->indiv.l);
+        if (bgzf_write_small(fp, tmp, data - tmp) < data - tmp)
+            return -1;
+        if (rec->shared.l > 0 &&
+            bgzf_write_small(fp, rec->shared.s, rec->shared.l) < rec->shared.l)
+            return -1;
+        if (rec->indiv.l > 0 &&
+            bgzf_write_small(fp, rec->indiv.s, rec->indiv.l) < rec->indiv.l)
+            return -1;
+    }
+
+    return 0;
+}
+
+static uint64_t unpack_unsigned(BGZF *fp, int *err)
+{
+    uint8_t data;
+    uint64_t val = 0;
+    uint32_t i = 0;
+
+    if (bgzf_read_small(fp, &data, sizeof(data)) <= 0)
+        goto short_read;
+
+    while (data & 0x80)
+    {
+        val |= (uint64_t)(data & 0x7f) << i;
+        i += 7;
+        if (bgzf_read_small(fp, &data, sizeof(data)) <= 0)
+            goto short_read;
+    }
+    val |= (uint64_t)data << i;
+    return val;
+
+ short_read:
+    *err = 1;
+    return 0;
+}
+
+static hts_pos_t unpack_hts_pos(BGZF *fp, int *err)
+{
+    uint64_t v = unpack_unsigned(fp, err);
+
+    if ((v & 1) == 0)
+        return (hts_pos_t)(v >> 1);
+    else
+        return -(hts_pos_t)(v >> 1) - 1;
+}
+
+static int read_packed_bcf(BGZF *fp, bcf1_t *dest)
+{
+    int err = 0;
+    packed_bcf_t tmp;
+    size_t len = tmp.data - (uint8_t *) &tmp.pos;
+    
+    bcf_clear(dest);
+    ssize_t got = bgzf_read_small(fp, &tmp.pos, len);
+    if (got == 0)
+        return -1;  // EOF
+    if (got < len)
+        return -2;  // Error or short read
+    dest->pos = tmp.pos;
+    dest->rid = tmp.rid;
+    dest->qual = tmp.qual;
+    dest->rlen = unpack_hts_pos(fp, &err);
+    dest->n_info = unpack_unsigned(fp, &err);
+    dest->n_allele = unpack_unsigned(fp, &err);
+    dest->n_fmt = unpack_unsigned(fp, &err);
+    dest->n_sample = unpack_unsigned(fp, &err);
+    len = unpack_unsigned(fp, &err);
+    if (ks_resize(&dest->shared, len) != 0)
+        return -2;
+    dest->shared.l = len;
+    len = unpack_unsigned(fp, &err);
+    if (ks_resize(&dest->indiv, len) != 0)
+        return -2;
+    dest->indiv.l = len;
+    err |= bgzf_read_small(fp, dest->shared.s, dest->shared.l) < dest->shared.l;
+    err |= bgzf_read_small(fp, dest->indiv.s, dest->indiv.l) < dest->indiv.l;
+    return err == 0 ? 0 : -2;
+}
 
+void open_tmp_file(args_t *args, blk_t *blk, int is_merged)
+{
     kstring_t str = {0,0,0};
-    ksprintf(&str, "%s/%05d.bcf", args->tmp_dir, (int)args->nblk);
-    blk->fname = str.s;
-    blk->rec   = NULL;
-    blk->fh    = NULL;
+    int tries = 1000;
 
-    htsFile *fh = hts_open(blk->fname, "wbu");
-    if ( fh == NULL ) clean_files_and_throw(args, "Cannot write %s: %s\n", blk->fname, strerror(errno));
-    if ( bcf_hdr_write(fh, args->hdr)!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__,blk->fname);
+    blk->fh = NULL;
+    blk->bgz = NULL;
+
+    do {
+        if (ksprintf(ks_clear(&str), "%s/%05zd%s",
+                     args->tmp_dir, args->tmp_count++,
+                     is_merged ? ".bcf" : "") < 0) {
+            clean_files_and_throw(args, "%s", strerror(errno));
+        }
+
+        if (is_merged)
+            blk->fh = hts_open(str.s, "wbx1");
+        else
+            blk->bgz = bgzf_open(str.s, "wx1");
+        if ( blk->fh == NULL && blk->bgz == NULL && (errno != EEXIST || --tries <= 0)) {
+            clean_files_and_throw(args, "Cannot write %s: %s\n",
+                                  str.s, strerror(errno));
+        }
+    } while (blk->fh == NULL && blk->bgz == NULL);
+
+    blk->fname = ks_release(&str);
+    blk->idx = args->tmp_count - 1;
+}
+
+void do_partial_merge(args_t *args);
+
+void buf_flush(args_t *args, bcf1_t *last_rec)
+{
+    if ( !args->nbuf ) return;
+
+    qsort(args->buf, args->nbuf, sizeof(*args->buf), cmp_packed_bcf_pos_ref_alt_stable);
+
+    if (args->tmp_layers[0] >= MAX_TMP_FILES_PER_LAYER)
+        do_partial_merge(args);
+
+    assert(args->nblk < MAX_TMP_FILES);
+    blk_t *blk = &args->blk[args->nblk];
+    blk->is_merged = 0;
+    args->nblk++;
+    args->tmp_layers[0]++;
 
+    assert(blk->fname == NULL && blk->fh == NULL && blk->bgz == NULL);
+
+    open_tmp_file(args, blk, 0);
     int i;
     for (i=0; i<args->nbuf; i++)
     {
-        if ( bcf_write(fh, args->hdr, args->buf[i])!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__,blk->fname);
+        if ( write_packed_bcf(blk->bgz, args->buf[i])!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__,blk->fname);
     }
-    if ( hts_close(fh)!=0 ) clean_files_and_throw(args, "[%s] Error: close failed .. %s\n", __func__,blk->fname);
+
+    if ( bgzf_close(blk->bgz)!=0 ) clean_files_and_throw(args, "[%s] Error: close failed .. %s\n", __func__,blk->fname);
+    blk->bgz = NULL;
 
     args->nbuf = 0;
     args->mem  = 0;
@@ -158,19 +435,38 @@ static inline uint8_t *_align_up(uint8_t *ptr)
     return (uint8_t*)(((size_t)ptr + 8 - 1) & ~((size_t)(8 - 1)));
 }
 
+#define varint_size(X) ((sizeof(X) * 8 + 7) / 7) // worst case
+
 void buf_push(args_t *args, bcf1_t *rec)
 {
-    size_t delta = sizeof(bcf1_t) + rec->shared.l + rec->indiv.l + rec->unpack_size[0] + rec->unpack_size[1]
-        + sizeof(*rec->d.allele)*rec->d.m_allele
-        + sizeof(bcf1_t*)       // args->buf
+    size_t delta = sizeof(rec->pos)
+        + sizeof(rec->rid)
+        + sizeof(rec->qual)
+        + varint_size(rec->rlen)
+        + varint_size(2) // n_info
+        + varint_size(2) // n_allele
+        + varint_size(1) // n_fmt
+        + varint_size(3) // n_sample
+        + varint_size(rec->shared.l)
+        + varint_size(rec->indiv.l)
+        + rec->shared.l + rec->indiv.l
+        + rec->unpack_size[1]   // Alleles
         + 8;                    // the number of _align_up() calls
 
     if ( delta > args->max_mem - args->mem )
     {
+        packed_bcf_t *tmp = malloc(sizeof(*tmp) + rec->unpack_size[1] * sizeof(bcf1_t *));
+        if (!tmp)
+            clean_files_and_throw(args, "[%s] Out of memory\n", __func__);
+        pack_bcf_data(tmp, rec, 1);
+
         args->nbuf++;
         hts_expand(bcf1_t*, args->nbuf, args->mbuf, args->buf);
-        args->buf[args->nbuf-1] = rec;
-        buf_flush(args);
+        args->buf[args->nbuf-1] = tmp;
+
+        buf_flush(args, rec);
+
+        free(tmp);
         bcf_destroy(rec);
         return;
     }
@@ -180,48 +476,13 @@ void buf_push(args_t *args, bcf1_t *rec)
 
     uint8_t *ptr_beg = args->mem_block + args->mem;
     uint8_t *ptr = _align_up(ptr_beg);
-    bcf1_t *new_rec = (bcf1_t*)ptr;
-    memcpy(new_rec,rec,sizeof(*rec));
-    ptr += sizeof(*rec);
-
-    // The array of allele pointers does not need alignment as bcf1_t is already padded to the biggest
-    // data type in the structure
-    char **allele = (char**)ptr;
-    ptr += rec->n_allele*sizeof(*allele);
-
-    // This is just to prevent valgrind from complaining about memcpy, unpack_size is a high-water mark
-    // and the end may be uninitialized
-    delta = rec->d.allele[rec->n_allele-1] - rec->d.allele[0];
-    while ( delta < rec->unpack_size[1] ) if ( !rec->d.als[delta++] ) break;
-    memcpy(ptr,rec->d.als,delta);
-    new_rec->d.als = (char*)ptr;
-    ptr = ptr + delta;
+    packed_bcf_t *packed_rec = (packed_bcf_t *) ptr;
 
-    int i;
-    for (i=0; i<rec->n_allele; i++) allele[i] = new_rec->d.als + (ptrdiff_t)(rec->d.allele[i] - rec->d.allele[0]);
-    new_rec->d.allele = allele;
-
-    memcpy(ptr,rec->shared.s,rec->shared.l);
-    new_rec->shared.s = (char*)ptr;
-    new_rec->shared.m = rec->shared.l;
-    ptr += rec->shared.l;
-
-    memcpy(ptr,rec->indiv.s,rec->indiv.l);
-    new_rec->indiv.s = (char*)ptr;
-    new_rec->indiv.m = rec->indiv.l;
-    ptr += rec->indiv.l;
-
-    // This is just to prevent valgrind from complaining about memcpy, unpack_size is a high-water mark
-    // and the end may be uninitialized
-    i = 0;
-    while ( i < rec->unpack_size[0] ) if ( !rec->d.id[i++] ) break;
-    memcpy(ptr,rec->d.id,i);
-    new_rec->d.id = (char*)ptr;
-    ptr += i;
+    ptr = pack_bcf_data(packed_rec, rec, 0);
 
     args->nbuf++;
     hts_expand(bcf1_t*, args->nbuf, args->mbuf, args->buf);
-    args->buf[args->nbuf-1] = new_rec;
+    args->buf[args->nbuf-1] = packed_rec;
 
     delta = ptr - ptr_beg;
     args->mem += delta;
@@ -248,11 +509,11 @@ void sort_blocks(args_t *args)
             bcf_destroy(rec);
             break;
         }
-        if ( rec->errcode ) clean_files_and_throw(args,"Error encountered while parsing the input at %s:%d\n",bcf_seqname(args->hdr,rec),rec->pos+1);
+        if ( rec->errcode ) clean_files_and_throw(args,"Error encountered while parsing the input at %s:%"PRIhts_pos"\n",bcf_seqname(args->hdr,rec),rec->pos+1);
         bcf_unpack(rec, BCF_UN_STR);
         buf_push(args, rec);
     }
-    buf_flush(args);
+    buf_flush(args, NULL);
     free(args->buf);
 
     if ( hts_close(in)!=0 ) clean_files_and_throw(args,"Close failed: %s\n", args->fname);
@@ -262,49 +523,80 @@ static inline int blk_is_smaller(blk_t **aptr, blk_t **bptr)
 {
     blk_t *a = *aptr;
     blk_t *b = *bptr;
-    int ret = cmp_bcf_pos(&a->rec, &b->rec);
+    int ret = cmp_bcf_pos_ref_alt(&a->rec, &b->rec);
     if ( ret < 0 ) return 1;
+    if (ret == 0 && a->idx < b->idx) return 1;
     return 0;
 }
 KHEAP_INIT(blk, blk_t*, blk_is_smaller)
 
 void blk_read(args_t *args, khp_blk_t *bhp, bcf_hdr_t *hdr, blk_t *blk)
 {
-    if ( !blk->fh ) return;
-    int ret = bcf_read(blk->fh, hdr, blk->rec);
+    int ret;
+    if (blk->is_merged)
+    {
+        if ( !blk->fh ) return;
+        ret = bcf_read(blk->fh, hdr, blk->rec);
+    }
+    else
+    {
+        if ( !blk->bgz ) return;
+        ret = read_packed_bcf(blk->bgz, blk->rec);
+    }
     if ( ret < -1 ) clean_files_and_throw(args, "Error reading %s\n", blk->fname);
     if ( ret == -1 )
     {
-        if ( hts_close(blk->fh)!=0 ) clean_files_and_throw(args, "Close failed: %s\n", blk->fname);
-        blk->fh = 0;
-        return;
+        if (blk->is_merged)
+        {
+            if ( hts_close(blk->fh)!=0 ) clean_files_and_throw(args, "Close failed: %s\n", blk->fname);
+            blk->fh = NULL;
+            return;
+        }
+        else
+        {
+            if ( bgzf_close(blk->bgz) != 0)
+                clean_files_and_throw(args, "Close failed: %s\n", blk->fname);
+            blk->bgz = NULL;
+            return;
+        }
     }
     bcf_unpack(blk->rec, BCF_UN_STR);
     khp_insert(blk, bhp, &blk);
 }
 
-void merge_blocks(args_t *args)
+void merge_blocks(args_t *args, htsFile *out, const char *output_fname,
+                  int idx_fmt, size_t from)
 {
-    fprintf(bcftools_stderr,"Merging %d temporary files\n", (int)args->nblk);
     khp_blk_t *bhp = khp_init(blk);
+    char *index_fn = NULL;
+    size_t i;
 
-    int i;
-    for (i=0; i<args->nblk; i++)
+    for (i=from; i<args->nblk; i++)
     {
-        blk_t *blk = args->blk + i;
-        blk->fh = hts_open(blk->fname, "r");
-        if ( !blk->fh ) clean_files_and_throw(args, "Could not read %s: %s\n", blk->fname, strerror(errno));
-        bcf_hdr_t *hdr = bcf_hdr_read(blk->fh);
-        bcf_hdr_destroy(hdr);
-        blk->rec = bcf_init();
+        blk_t *blk = &args->blk[i];
+        if (blk->is_merged)
+        {
+            blk->fh = hts_open(blk->fname, "r");
+            if ( !blk->fh ) clean_files_and_throw(args, "Could not read %s: %s\n", blk->fname, strerror(errno));
+            bcf_hdr_t *hdr = bcf_hdr_read(blk->fh);
+            bcf_hdr_destroy(hdr);
+        }
+        else
+        {
+            blk->bgz = bgzf_open(blk->fname, "r");
+            if (!blk->bgz)
+                clean_files_and_throw(args, "Could not read %s: %s\n", blk->fname, strerror(errno));
+        }
         blk_read(args, bhp, args->hdr, blk);
     }
 
-    char wmode[8];
-    set_wmode(wmode,args->output_type,args->output_fname,args->clevel);
-    htsFile *out = hts_open(args->output_fname ? args->output_fname : "-", wmode);
-    if ( bcf_hdr_write(out, args->hdr)!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__,args->output_fname);
-    if ( args->write_index && init_index(out,args->hdr,args->output_fname,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->output_fname);
+    if ( bcf_hdr_write(out, args->hdr)!=0 ) clean_files_and_throw(args, "[%s] Error: cannot write to %s\n", __func__, output_fname);
+
+    if (idx_fmt) {
+        if ( init_index2(out,args->hdr,output_fname,&index_fn,idx_fmt)<0 )
+            error("Error: failed to initialise index for %s\n",output_fname);
+    }
+
     while ( bhp->ndat )
     {
         blk_t *blk = bhp->dat[0];
@@ -312,22 +604,93 @@ void merge_blocks(args_t *args)
         khp_delete(blk, bhp);
         blk_read(args, bhp, args->hdr, blk);
     }
-    if ( args->write_index )
+    if ( idx_fmt )
     {
         if ( bcf_idx_save(out)<0 )
         {
-            if ( hts_close(out)!=0 ) error("Error: close failed .. %s\n", args->output_fname?args->output_fname:"bcftools_stdout");
-            error("Error: cannot write to index %s\n", args->index_fn);
+            if ( hts_close(out)!=0 ) error("Error: close failed .. %s\n", output_fname);
+            error("Error: cannot write to index %s\n", index_fn);
         }
-        free(args->index_fn);
+        free(index_fn);
     }
-    if ( hts_close(out)!=0 ) clean_files_and_throw(args, "Close failed: %s\n", args->output_fname);
 
-    clean_files(args);
+    for (i = from; i < args->nblk; i++)
+    {
+        blk_t *blk = &args->blk[i];
+        if (unlink(blk->fname) != 0)
+            clean_files_and_throw(args, "Couldn't remove temporary file %s\n", blk->fname);
+        free(blk->fname);
+        blk->fname = NULL;
+    }
 
-    free(args->blk);
     khp_destroy(blk, bhp);
+}
+
+void do_partial_merge(args_t *args)
+{
+    uint32_t to_layer = 0;
+    size_t to_merge = 0;
+
+    // Temp. files are arranged in layers of at most MAX_TMP_FILES_PER_LAYER.
+    // When a layer is full, it is merged into the next layer up.  Each
+    // layer will therefore contain files with exponentially more records
+    // then the previous one, but will be merged exponentially less frequently.
+    // The result is that the overall complexity will remain O(n*log(n))
+    // even if we need to do lots of partial merges.
+
+    while (to_layer < MERGE_LAYERS
+           && args->tmp_layers[to_layer] >= MAX_TMP_FILES_PER_LAYER)
+    {
+        to_merge += args->tmp_layers[to_layer];
+        args->tmp_layers[to_layer] = 0;
+        to_layer++;
+    }
+
+    assert(to_merge > 0 && to_merge <= args->nblk);
+
+    if (to_layer == MERGE_LAYERS) {
+        // Edge case - if we've got here, we've completely used the
+        // temp file allocation, so merge absolutely everything and
+        // leave one file at the highest level.  Strictly this breaks
+        // the O(n*log(n)) complexity, but unless MERGE_LAYERS and
+        // MAX_TMP_FILES_PER_LAYER are too small it would take so long
+        // to get here it should never actually happen...
+        assert(to_merge == MAX_TMP_FILES_PER_LAYER * MERGE_LAYERS);
+        to_layer = MERGE_LAYERS - 1;
+    }
+
+    blk_t tmp = { NULL };
+    open_tmp_file(args, &tmp, 1);
+    merge_blocks(args, tmp.fh, tmp.fname, 0, args->nblk - to_merge);
+    if (hts_close(tmp.fh) != 0)
+        clean_files_and_throw(args, "Close failed: %s\n", tmp.fname);
+
+    args->nblk -= to_merge;
+    assert(args->blk[args->nblk].fh == NULL);
+    assert(args->blk[args->nblk].fname == NULL);
+    args->blk[args->nblk].is_merged = 1;
+    args->blk[args->nblk].idx = tmp.idx;
+    args->blk[args->nblk++].fname = tmp.fname;
+    args->tmp_layers[to_layer]++;
+}
+
+void merge_to_output(args_t *args)
+{
+    char wmode[8] = { 0 };
+    set_wmode(wmode,args->output_type,args->output_fname,args->clevel);
+    const char *output_fname = args->output_fname ? args->output_fname : "-";
+
+    htsFile *out = hts_open(output_fname, wmode);
+    if (!out) clean_files_and_throw(args, "[%s] Error: cannot open %s\n", __func__, output_fname);
+
+    fprintf(bcftools_stderr,"Merging %zd temporary files\n", args->nblk);
+    merge_blocks(args, out, output_fname, args->write_index, 0);
     fprintf(bcftools_stderr,"Done\n");
+
+    if ( hts_close(out)!=0 )
+        clean_files_and_throw(args, "Close failed: %s\n", output_fname);
+
+    clean_files(args);
 }
 
 static void usage(args_t *args)
@@ -339,7 +702,6 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "Options:\n");
     fprintf(bcftools_stderr, "    -m, --max-mem FLOAT[kMG]       maximum memory to use [768M]\n");    // using metric units, 1M=1e6
     fprintf(bcftools_stderr, "    -o, --output FILE              output file name [bcftools_stdout]\n");
-    fprintf(bcftools_stderr, "    -O, --output-type b|u|z|v      b: compressed BCF, u: uncompressed BCF, z: compressed VCF, v: uncompressed VCF [v]\n");
     fprintf(bcftools_stderr, "    -O, --output-type u|b|v|z[0-9] u/b: un/compressed BCF, v/z: un/compressed VCF, 0-9: compression level [v]\n");
 
 #ifdef _WIN32
@@ -347,7 +709,7 @@ static void usage(args_t *args)
 #else
     fprintf(bcftools_stderr, "    -T, --temp-dir DIR             temporary files [/tmp/bcftools.XXXXXX]\n");
 #endif
-    fprintf(bcftools_stderr, "        --write-index              Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "    -W, --write-index[=FMT]        Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     bcftools_exit(1);
 }
@@ -363,14 +725,23 @@ size_t parse_mem_string(const char *str)
     return mem;
 }
 
-void mkdir_p(const char *fmt, ...);
 static void init(args_t *args)
 {
+    size_t i;
     args->max_mem *= 0.9;
     args->mem_block = malloc(args->max_mem);
     if ( !args->mem_block ) error("Error: could not allocate %zu bytes of memory, try reducing --max-mem\n",args->max_mem);
     args->mem = 0;
 
+    for (i = 0; i < MAX_TMP_FILES; i++)
+    {
+        args->blk[i].fname = NULL;
+        args->blk[i].rec = bcf_init();
+        if (!args->blk[i].rec)
+            clean_files_and_throw(args,"Couldn't allocate bcf record\n");
+    }
+
+
     args->tmp_dir = init_tmp_prefix(args->tmp_dir);
 
 #ifdef _WIN32
@@ -410,11 +781,11 @@ int main_sort(int argc, char *argv[])
         {"output-file",required_argument,NULL,'o'},
         {"output",required_argument,NULL,'o'},
         {"help",no_argument,NULL,'h'},
-        {"write-index",no_argument,NULL,1},
+        {"write-index",optional_argument,NULL,'W'},
         {0,0,0,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "m:T:O:o:h?",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "m:T:O:o:W::h?",loptions,NULL)) >= 0)
     {
         switch (c)
         {
@@ -439,7 +810,10 @@ int main_sort(int argc, char *argv[])
                           if ( *tmp || args->clevel<0 || args->clevel>9 ) error("Could not parse argument: --compression-level %s\n", optarg+1);
                       }
                       break;
-            case  1 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case 'h':
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
@@ -455,7 +829,7 @@ int main_sort(int argc, char *argv[])
 
     init(args);
     sort_blocks(args);
-    merge_blocks(args);
+    merge_to_output(args);
     destroy(args);
 
     return 0;
index e2744ab3c662cf92b8c7355a0918165faf296565..38b4caf510464eb29e41a6381efe923e696db685 100644 (file)
@@ -1,6 +1,6 @@
 /*  vcfstats.c -- Produces stats which can be plotted using plot-vcfstats.
 
-    Copyright (C) 2012-2023 Genome Research Ltd.
+    Copyright (C) 2012-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -408,7 +408,8 @@ static void init_user_stats(args_t *args, bcf_hdr_t *hdr, stats_t *stats)
 {
     stats->nusr = args->nusr;
     stats->usr = (user_stats_t*)malloc(sizeof(user_stats_t)*args->nusr);
-    memcpy(stats->usr,args->usr,args->nusr*sizeof(user_stats_t));
+    if (args->nusr)
+        memcpy(stats->usr,args->usr,args->nusr*sizeof(user_stats_t));
     int i;
     for (i=0; i<stats->nusr; i++)
     {
@@ -894,7 +895,7 @@ static inline int get_ad(bcf1_t *line, bcf_fmt_t *ad_fmt_ptr, int ismpl, int *ia
     *ial = 0;
     #define BRANCH_INT(type_t,missing,vector_end) { \
         type_t *ptr = (type_t *) (ad_fmt_ptr->p + ad_fmt_ptr->size*ismpl); \
-        for (iv=1; iv<ad_fmt_ptr->n; iv++) \
+        for (iv=1; iv<ad_fmt_ptr->n && iv<line->n_allele; iv++) \
         { \
             if ( ptr[iv]==vector_end ) break; \
             if ( ptr[iv]==missing ) continue; \
@@ -938,9 +939,12 @@ static inline void update_dvaf(stats_t *stats, bcf1_t *line, int ial, float vaf)
 #define vaf2bin(vaf) ((int)nearbyintf((vaf)/0.05))
 static inline void update_vaf(vaf_t *smpl_vaf, bcf1_t *line, int ial, float vaf)
 {
-    int idx = vaf2bin(vaf);
-    if ( bcf_get_variant_type(line,ial)==VCF_SNP ) smpl_vaf->snv[idx]++;
-    else smpl_vaf->indel[idx]++;
+    if ( vaf>=0 && vaf<=1 )
+    {
+        int idx = vaf2bin(vaf);
+        if ( bcf_get_variant_type(line,ial)==VCF_SNP ) smpl_vaf->snv[idx]++;
+        else smpl_vaf->indel[idx]++;
+    }
 }
 
 static inline int calc_sample_depth(args_t *args, int ismpl, bcf_fmt_t *ad_fmt_ptr, bcf_fmt_t *dp_fmt_ptr)
@@ -1373,7 +1377,10 @@ static void print_stats(args_t *args)
         printf("SN\t%d\tnumber of multiallelic sites:\t%"PRIu64"\n", id, stats->n_mals);
         printf("SN\t%d\tnumber of multiallelic SNP sites:\t%"PRIu64"\n", id, stats->n_snp_mals);
     }
-    printf("# TSTV, transitions/transversions:\n# TSTV\t[2]id\t[3]ts\t[4]tv\t[5]ts/tv\t[6]ts (1st ALT)\t[7]tv (1st ALT)\t[8]ts/tv (1st ALT)\n");
+    printf("# TSTV, transitions/transversions\n"
+           "#   - transitions, see https://en.wikipedia.org/wiki/Transition_(genetics)\n"
+           "#   - transversions, see https://en.wikipedia.org/wiki/Transversion\n");
+    printf("# TSTV\t[2]id\t[3]ts\t[4]tv\t[5]ts/tv\t[6]ts (1st ALT)\t[7]tv (1st ALT)\t[8]ts/tv (1st ALT)\n");
     for (id=0; id<args->nstats; id++)
     {
         stats_t *stats = &args->stats[id];
@@ -1393,7 +1400,9 @@ static void print_stats(args_t *args)
     }
     if ( args->indel_ctx )
     {
-        printf("# ICS, Indel context summary:\n# ICS\t[2]id\t[3]repeat-consistent\t[4]repeat-inconsistent\t[5]not applicable\t[6]c/(c+i) ratio\n");
+        printf("# ICS, Indel context:\n"
+               "#   - repeat-consistent, inconsistent and n/a: experimental and useless stats [DEPRECATED]\n");
+        printf("# ICS\t[2]id\t[3]repeat-consistent\t[4]repeat-inconsistent\t[5]not applicable\t[6]c/(c+i) ratio\n");
         for (id=0; id<args->nstats; id++)
         {
             int nc = 0, ni = 0, na = args->stats[id].n_repeat_na;
@@ -1404,7 +1413,9 @@ static void print_stats(args_t *args)
             }
             printf("ICS\t%d\t%d\t%d\t%d\t%.4f\n", id, nc,ni,na,nc+ni ? (float)nc/(nc+ni) : 0.0);
         }
-        printf("# ICL, Indel context by length:\n# ICL\t[2]id\t[3]length of repeat element\t[4]repeat-consistent deletions)\t[5]repeat-inconsistent deletions\t[6]consistent insertions\t[7]inconsistent insertions\t[8]c/(c+i) ratio\n");
+        printf("# ICL, Indel context by length:\n"
+               "#   - repeat-consistent, inconsistent and n/a: experimental and useless stats [DEPRECATED]\n");
+        printf("# ICL\t[2]id\t[3]length of repeat element\t[4]repeat-consistent deletions)\t[5]repeat-inconsistent deletions\t[6]consistent insertions\t[7]inconsistent insertions\t[8]c/(c+i) ratio\n");
         for (id=0; id<args->nstats; id++)
         {
             for (i=1; i<IRC_RLEN; i++)
@@ -1416,7 +1427,12 @@ static void print_stats(args_t *args)
             }
         }
     }
-    printf("# SiS, Singleton stats:\n# SiS\t[2]id\t[3]allele count\t[4]number of SNPs\t[5]number of transitions\t[6]number of transversions\t[7]number of indels\t[8]repeat-consistent\t[9]repeat-inconsistent\t[10]not applicable\n");
+    printf("# SiS, Singleton stats:\n"
+           "#   - allele count, i.e. the number of singleton genotypes (AC=1)\n"
+           "#   - number of transitions, see above\n"
+           "#   - number of transversions, see above\n"
+           "#   - repeat-consistent, inconsistent and n/a: experimental and useless stats [DEPRECATED]\n");
+    printf("# SiS\t[2]id\t[3]allele count\t[4]number of SNPs\t[5]number of transitions\t[6]number of transversions\t[7]number of indels\t[8]repeat-consistent\t[9]repeat-inconsistent\t[10]not applicable\n");
     for (id=0; id<args->nstats; id++)
     {
         stats_t *stats = &args->stats[id];
@@ -1513,7 +1529,7 @@ static void print_stats(args_t *args)
             {
                 if ( usr->vals_ts[j]+usr->vals_tv[j] == 0 ) continue;   // skip empty bins
                 float val = usr->min + (usr->max - usr->min)*j/(usr->nbins-1);
-                const char *fmt = usr->type==BCF_HT_REAL ? "USR:%s/%d\t%d\t%e\t%"PRIu64"\t%"PRIu64"\t%"PRIu64"\n" : "USR:%s/%d\t%d\t%.0f\t%"PRIu64"\t%"PRIu64"\t%"PRIu64"\n";
+                const char * const fmt = usr->type==BCF_HT_REAL ? "USR:%s/%d\t%d\t%e\t%"PRIu64"\t%"PRIu64"\t%"PRIu64"\n" : "USR:%s/%d\t%d\t%.0f\t%"PRIu64"\t%"PRIu64"\t%"PRIu64"\n";
                 printf(fmt,usr->tag,usr->idx,id,val,usr->vals_ts[j]+usr->vals_tv[j],usr->vals_ts[j],usr->vals_tv[j]);
             }
         }
@@ -1731,6 +1747,13 @@ static void print_stats(args_t *args)
         }
     }
 
+    printf("# DP, depth:\n"
+           "#   - set id, see above\n"
+           "#   - the depth bin, corresponds to the depth (unless --depth was given)\n"
+           "#   - number of genotypes with this depth (zero unless -s/-S was given)\n"
+           "#   - fraction of genotypes with this depth (zero unless -s/-S was given)\n"
+           "#   - number of sites with this depth\n"
+           "#   - fraction of sites with this depth\n");
     printf("# DP, Depth distribution\n# DP\t[2]id\t[3]bin\t[4]number of genotypes\t[5]fraction of genotypes (%%)\t[6]number of sites\t[7]fraction of sites (%%)\n");
     for (id=0; id<args->nstats; id++)
     {
index 11db1d1cc05afec64dda8cf3de392cc8f27f9e6b..946032ed5f73f7df5c69240986c2b18980b57f8a 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  vcfstats.c -- Produces stats which can be plotted using plot-vcfstats.
 
-    Copyright (C) 2012-2023 Genome Research Ltd.
+    Copyright (C) 2012-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -410,7 +410,8 @@ static void init_user_stats(args_t *args, bcf_hdr_t *hdr, stats_t *stats)
 {
     stats->nusr = args->nusr;
     stats->usr = (user_stats_t*)malloc(sizeof(user_stats_t)*args->nusr);
-    memcpy(stats->usr,args->usr,args->nusr*sizeof(user_stats_t));
+    if (args->nusr)
+        memcpy(stats->usr,args->usr,args->nusr*sizeof(user_stats_t));
     int i;
     for (i=0; i<stats->nusr; i++)
     {
@@ -896,7 +897,7 @@ static inline int get_ad(bcf1_t *line, bcf_fmt_t *ad_fmt_ptr, int ismpl, int *ia
     *ial = 0;
     #define BRANCH_INT(type_t,missing,vector_end) { \
         type_t *ptr = (type_t *) (ad_fmt_ptr->p + ad_fmt_ptr->size*ismpl); \
-        for (iv=1; iv<ad_fmt_ptr->n; iv++) \
+        for (iv=1; iv<ad_fmt_ptr->n && iv<line->n_allele; iv++) \
         { \
             if ( ptr[iv]==vector_end ) break; \
             if ( ptr[iv]==missing ) continue; \
@@ -940,9 +941,12 @@ static inline void update_dvaf(stats_t *stats, bcf1_t *line, int ial, float vaf)
 #define vaf2bin(vaf) ((int)nearbyintf((vaf)/0.05))
 static inline void update_vaf(vaf_t *smpl_vaf, bcf1_t *line, int ial, float vaf)
 {
-    int idx = vaf2bin(vaf);
-    if ( bcf_get_variant_type(line,ial)==VCF_SNP ) smpl_vaf->snv[idx]++;
-    else smpl_vaf->indel[idx]++;
+    if ( vaf>=0 && vaf<=1 )
+    {
+        int idx = vaf2bin(vaf);
+        if ( bcf_get_variant_type(line,ial)==VCF_SNP ) smpl_vaf->snv[idx]++;
+        else smpl_vaf->indel[idx]++;
+    }
 }
 
 static inline int calc_sample_depth(args_t *args, int ismpl, bcf_fmt_t *ad_fmt_ptr, bcf_fmt_t *dp_fmt_ptr)
@@ -1375,7 +1379,10 @@ static void print_stats(args_t *args)
         fprintf(bcftools_stdout, "SN\t%d\tnumber of multiallelic sites:\t%"PRIu64"\n", id, stats->n_mals);
         fprintf(bcftools_stdout, "SN\t%d\tnumber of multiallelic SNP sites:\t%"PRIu64"\n", id, stats->n_snp_mals);
     }
-    fprintf(bcftools_stdout, "# TSTV, transitions/transversions:\n# TSTV\t[2]id\t[3]ts\t[4]tv\t[5]ts/tv\t[6]ts (1st ALT)\t[7]tv (1st ALT)\t[8]ts/tv (1st ALT)\n");
+    fprintf(bcftools_stdout, "# TSTV, transitions/transversions\n"
+           "#   - transitions, see https://en.wikipedia.org/wiki/Transition_(genetics)\n"
+           "#   - transversions, see https://en.wikipedia.org/wiki/Transversion\n");
+    fprintf(bcftools_stdout, "# TSTV\t[2]id\t[3]ts\t[4]tv\t[5]ts/tv\t[6]ts (1st ALT)\t[7]tv (1st ALT)\t[8]ts/tv (1st ALT)\n");
     for (id=0; id<args->nstats; id++)
     {
         stats_t *stats = &args->stats[id];
@@ -1395,7 +1402,9 @@ static void print_stats(args_t *args)
     }
     if ( args->indel_ctx )
     {
-        fprintf(bcftools_stdout, "# ICS, Indel context summary:\n# ICS\t[2]id\t[3]repeat-consistent\t[4]repeat-inconsistent\t[5]not applicable\t[6]c/(c+i) ratio\n");
+        fprintf(bcftools_stdout, "# ICS, Indel context:\n"
+               "#   - repeat-consistent, inconsistent and n/a: experimental and useless stats [DEPRECATED]\n");
+        fprintf(bcftools_stdout, "# ICS\t[2]id\t[3]repeat-consistent\t[4]repeat-inconsistent\t[5]not applicable\t[6]c/(c+i) ratio\n");
         for (id=0; id<args->nstats; id++)
         {
             int nc = 0, ni = 0, na = args->stats[id].n_repeat_na;
@@ -1406,7 +1415,9 @@ static void print_stats(args_t *args)
             }
             fprintf(bcftools_stdout, "ICS\t%d\t%d\t%d\t%d\t%.4f\n", id, nc,ni,na,nc+ni ? (float)nc/(nc+ni) : 0.0);
         }
-        fprintf(bcftools_stdout, "# ICL, Indel context by length:\n# ICL\t[2]id\t[3]length of repeat element\t[4]repeat-consistent deletions)\t[5]repeat-inconsistent deletions\t[6]consistent insertions\t[7]inconsistent insertions\t[8]c/(c+i) ratio\n");
+        fprintf(bcftools_stdout, "# ICL, Indel context by length:\n"
+               "#   - repeat-consistent, inconsistent and n/a: experimental and useless stats [DEPRECATED]\n");
+        fprintf(bcftools_stdout, "# ICL\t[2]id\t[3]length of repeat element\t[4]repeat-consistent deletions)\t[5]repeat-inconsistent deletions\t[6]consistent insertions\t[7]inconsistent insertions\t[8]c/(c+i) ratio\n");
         for (id=0; id<args->nstats; id++)
         {
             for (i=1; i<IRC_RLEN; i++)
@@ -1418,7 +1429,12 @@ static void print_stats(args_t *args)
             }
         }
     }
-    fprintf(bcftools_stdout, "# SiS, Singleton stats:\n# SiS\t[2]id\t[3]allele count\t[4]number of SNPs\t[5]number of transitions\t[6]number of transversions\t[7]number of indels\t[8]repeat-consistent\t[9]repeat-inconsistent\t[10]not applicable\n");
+    fprintf(bcftools_stdout, "# SiS, Singleton stats:\n"
+           "#   - allele count, i.e. the number of singleton genotypes (AC=1)\n"
+           "#   - number of transitions, see above\n"
+           "#   - number of transversions, see above\n"
+           "#   - repeat-consistent, inconsistent and n/a: experimental and useless stats [DEPRECATED]\n");
+    fprintf(bcftools_stdout, "# SiS\t[2]id\t[3]allele count\t[4]number of SNPs\t[5]number of transitions\t[6]number of transversions\t[7]number of indels\t[8]repeat-consistent\t[9]repeat-inconsistent\t[10]not applicable\n");
     for (id=0; id<args->nstats; id++)
     {
         stats_t *stats = &args->stats[id];
@@ -1515,7 +1531,7 @@ static void print_stats(args_t *args)
             {
                 if ( usr->vals_ts[j]+usr->vals_tv[j] == 0 ) continue;   // skip empty bins
                 float val = usr->min + (usr->max - usr->min)*j/(usr->nbins-1);
-                const char *fmt = usr->type==BCF_HT_REAL ? "USR:%s/%d\t%d\t%e\t%"PRIu64"\t%"PRIu64"\t%"PRIu64"\n" : "USR:%s/%d\t%d\t%.0f\t%"PRIu64"\t%"PRIu64"\t%"PRIu64"\n";
+                const char * const fmt = usr->type==BCF_HT_REAL ? "USR:%s/%d\t%d\t%e\t%"PRIu64"\t%"PRIu64"\t%"PRIu64"\n" : "USR:%s/%d\t%d\t%.0f\t%"PRIu64"\t%"PRIu64"\t%"PRIu64"\n";
                 fprintf(bcftools_stdout, fmt,usr->tag,usr->idx,id,val,usr->vals_ts[j]+usr->vals_tv[j],usr->vals_ts[j],usr->vals_tv[j]);
             }
         }
@@ -1733,6 +1749,13 @@ static void print_stats(args_t *args)
         }
     }
 
+    fprintf(bcftools_stdout, "# DP, depth:\n"
+           "#   - set id, see above\n"
+           "#   - the depth bin, corresponds to the depth (unless --depth was given)\n"
+           "#   - number of genotypes with this depth (zero unless -s/-S was given)\n"
+           "#   - fraction of genotypes with this depth (zero unless -s/-S was given)\n"
+           "#   - number of sites with this depth\n"
+           "#   - fraction of sites with this depth\n");
     fprintf(bcftools_stdout, "# DP, Depth distribution\n# DP\t[2]id\t[3]bin\t[4]number of genotypes\t[5]fraction of genotypes (%%)\t[6]number of sites\t[7]fraction of sites (%%)\n");
     for (id=0; id<args->nstats; id++)
     {
index e09efa0bc0314faf2908d22eb689079786cb2f71..58063ebb4de0a6f82050231708adad544819e0a3 100644 (file)
@@ -36,6 +36,7 @@ THE SOFTWARE.  */
 #include <htslib/vcf.h>
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
+#include <htslib/kbitset.h>
 #include "bcftools.h"
 #include "filter.h"
 #include "htslib/khash_str2int.h"
@@ -78,6 +79,7 @@ typedef struct _args_t
     int record_cmd_line;
     char *index_fn;
     int write_index;
+    int trim_star_allele;
     htsFile *out;
 }
 args_t;
@@ -456,6 +458,19 @@ int subset_vcf(args_t *args, bcf1_t *line)
         int ret = bcf_trim_alleles(args->hsub ? args->hsub : args->hdr, line);
         if ( ret<0 ) error("Error: Could not trim alleles at %s:%"PRId64"\n", bcf_seqname(args->hsub ? args->hsub : args->hdr, line), (int64_t) line->pos+1);
     }
+    if (args->trim_star_allele)
+    {
+        int iunseen;
+        if ( args->trim_star_allele && (line->n_allele > 2 || args->trim_star_allele > 1) && (iunseen=get_unseen_allele(line)) && iunseen>0 )
+        {
+            // the unobserved star allele should be trimmed, either it is variant site or trimming of all sites was requested
+            kbitset_t *rm_set = kbs_init(line->n_allele);
+            kbs_insert(rm_set, iunseen);
+            if ( bcf_remove_allele_set(args->hdr,line,rm_set) )
+                error("[%s] Error: failed to trim the unobserved allele at %s:%"PRIhts_pos"\n",__func__,bcf_seqname(args->hdr,line),line->pos+1);
+            kbs_destroy(rm_set);
+        }
+    }
     if (args->phased) {
         int phased = bcf_all_phased(args->hdr, line);
         if (args->phased == FLT_INCLUDE && !phased) { return 0; } // skip unphased
@@ -512,6 +527,7 @@ static void usage(args_t *args)
     fprintf(stderr, "        --threads INT                 Use multithreading with INT worker threads [0]\n");
     fprintf(stderr, "\n");
     fprintf(stderr, "Subset options:\n");
+    fprintf(stderr, "    -A, --trim-unseen-allele          Remove '<*>' or '<NON_REF>' at variant (-A) or at all (-AA) sites\n");
     fprintf(stderr, "    -a, --trim-alt-alleles            Trim ALT alleles not seen in the genotype fields (or their subset with -s/-S)\n");
     fprintf(stderr, "    -I, --no-update                   Do not (re)calculate INFO fields for the subset (currently INFO/AC and INFO/AN)\n");
     fprintf(stderr, "    -s, --samples [^]LIST             Comma separated list of samples to include (or exclude with \"^\" prefix). Be careful\n");
@@ -534,7 +550,7 @@ static void usage(args_t *args)
     fprintf(stderr, "    -u/U, --uncalled/--exclude-uncalled    Select/exclude sites without a called genotype\n");
     fprintf(stderr, "    -v/V, --types/--exclude-types LIST     Select/exclude comma-separated list of variant types: snps,indels,mnps,ref,bnd,other [null]\n");
     fprintf(stderr, "    -x/X, --private/--exclude-private      Select/exclude sites where the non-reference alleles are exclusive (private) to the subset samples\n");
-    fprintf(stderr, "          --write-index                    Automatically index the output files [off]\n");
+    fprintf(stderr, "    -W,   --write-index[=FMT]              Automatically index the output files [off]\n");
     fprintf(stderr, "\n");
     exit(1);
 }
@@ -568,6 +584,7 @@ int main_vcfview(int argc, char *argv[])
         {"exclude",required_argument,NULL,'e'},
         {"include",required_argument,NULL,'i'},
         {"trim-alt-alleles",no_argument,NULL,'a'},
+        {"trim-unseen-allele",no_argument,NULL,'A'},
         {"no-update",no_argument,NULL,'I'},
         {"drop-genotypes",no_argument,NULL,'G'},
         {"private",no_argument,NULL,'x'},
@@ -600,11 +617,11 @@ int main_vcfview(int argc, char *argv[])
         {"phased",no_argument,NULL,'p'},
         {"exclude-phased",no_argument,NULL,'P'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,10},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "l:t:T:r:R:o:O:s:S:Gf:knv:V:m:M:auUhHc:C:Ii:e:xXpPq:Q:g:",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "l:t:T:r:R:o:O:s:S:Gf:knv:V:m:M:aAuUhHc:C:Ii:e:xXpPq:Q:g:W::",loptions,NULL)) >= 0)
     {
         char allele_type[9] = "nref";
         switch (c)
@@ -646,6 +663,7 @@ int main_vcfview(int argc, char *argv[])
             case 'S': args->sample_names = optarg; args->sample_is_file = 1; break;
             case  1 : args->force_samples = 1; break;
             case 'a': args->trim_alts = 1; args->calc_ac = 1; break;
+            case 'A': args->trim_star_allele++; break;
             case 'I': args->update_info = 0; break;
             case 'G': args->sites_only = 1; break;
 
@@ -732,7 +750,10 @@ int main_vcfview(int argc, char *argv[])
                 break;
             case  9 : args->n_threads = strtol(optarg, 0, 0); break;
             case  8 : args->record_cmd_line = 0; break;
-            case 10 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
         }
@@ -789,7 +810,9 @@ int main_vcfview(int argc, char *argv[])
     else if ( args->output_type & FT_BCF )
         error("BCF output requires header, cannot proceed with -H\n");
 
-    if ( args->write_index && init_index(args->out,out_hdr,args->fn_out,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->fn_out);
+    if ( init_index2(args->out,out_hdr,args->fn_out, &args->index_fn,
+                     args->write_index) < 0 )
+        error("Error: failed to initialise index for %s\n",args->fn_out);
 
     int ret = 0;
     if (!args->header_only)
index 1485b1e65f87ce4a30f5c69313e9290015b619bb..d430bca0be5eb79f2255fbb4018f5a6c80f46df5 100644 (file)
@@ -38,6 +38,7 @@ THE SOFTWARE.  */
 #include <htslib/vcf.h>
 #include <htslib/synced_bcf_reader.h>
 #include <htslib/vcfutils.h>
+#include <htslib/kbitset.h>
 #include "bcftools.h"
 #include "filter.h"
 #include "htslib/khash_str2int.h"
@@ -80,6 +81,7 @@ typedef struct _args_t
     int record_cmd_line;
     char *index_fn;
     int write_index;
+    int trim_star_allele;
     htsFile *out;
 }
 args_t;
@@ -458,6 +460,19 @@ int subset_vcf(args_t *args, bcf1_t *line)
         int ret = bcf_trim_alleles(args->hsub ? args->hsub : args->hdr, line);
         if ( ret<0 ) error("Error: Could not trim alleles at %s:%"PRId64"\n", bcf_seqname(args->hsub ? args->hsub : args->hdr, line), (int64_t) line->pos+1);
     }
+    if (args->trim_star_allele)
+    {
+        int iunseen;
+        if ( args->trim_star_allele && (line->n_allele > 2 || args->trim_star_allele > 1) && (iunseen=get_unseen_allele(line)) && iunseen>0 )
+        {
+            // the unobserved star allele should be trimmed, either it is variant site or trimming of all sites was requested
+            kbitset_t *rm_set = kbs_init(line->n_allele);
+            kbs_insert(rm_set, iunseen);
+            if ( bcf_remove_allele_set(args->hdr,line,rm_set) )
+                error("[%s] Error: failed to trim the unobserved allele at %s:%"PRIhts_pos"\n",__func__,bcf_seqname(args->hdr,line),line->pos+1);
+            kbs_destroy(rm_set);
+        }
+    }
     if (args->phased) {
         int phased = bcf_all_phased(args->hdr, line);
         if (args->phased == FLT_INCLUDE && !phased) { return 0; } // skip unphased
@@ -514,6 +529,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "        --threads INT                 Use multithreading with INT worker threads [0]\n");
     fprintf(bcftools_stderr, "\n");
     fprintf(bcftools_stderr, "Subset options:\n");
+    fprintf(bcftools_stderr, "    -A, --trim-unseen-allele          Remove '<*>' or '<NON_REF>' at variant (-A) or at all (-AA) sites\n");
     fprintf(bcftools_stderr, "    -a, --trim-alt-alleles            Trim ALT alleles not seen in the genotype fields (or their subset with -s/-S)\n");
     fprintf(bcftools_stderr, "    -I, --no-update                   Do not (re)calculate INFO fields for the subset (currently INFO/AC and INFO/AN)\n");
     fprintf(bcftools_stderr, "    -s, --samples [^]LIST             Comma separated list of samples to include (or exclude with \"^\" prefix). Be careful\n");
@@ -536,7 +552,7 @@ static void usage(args_t *args)
     fprintf(bcftools_stderr, "    -u/U, --uncalled/--exclude-uncalled    Select/exclude sites without a called genotype\n");
     fprintf(bcftools_stderr, "    -v/V, --types/--exclude-types LIST     Select/exclude comma-separated list of variant types: snps,indels,mnps,ref,bnd,other [null]\n");
     fprintf(bcftools_stderr, "    -x/X, --private/--exclude-private      Select/exclude sites where the non-reference alleles are exclusive (private) to the subset samples\n");
-    fprintf(bcftools_stderr, "          --write-index                    Automatically index the output files [off]\n");
+    fprintf(bcftools_stderr, "    -W,   --write-index[=FMT]              Automatically index the output files [off]\n");
     fprintf(bcftools_stderr, "\n");
     bcftools_exit(1);
 }
@@ -570,6 +586,7 @@ int main_vcfview(int argc, char *argv[])
         {"exclude",required_argument,NULL,'e'},
         {"include",required_argument,NULL,'i'},
         {"trim-alt-alleles",no_argument,NULL,'a'},
+        {"trim-unseen-allele",no_argument,NULL,'A'},
         {"no-update",no_argument,NULL,'I'},
         {"drop-genotypes",no_argument,NULL,'G'},
         {"private",no_argument,NULL,'x'},
@@ -602,11 +619,11 @@ int main_vcfview(int argc, char *argv[])
         {"phased",no_argument,NULL,'p'},
         {"exclude-phased",no_argument,NULL,'P'},
         {"no-version",no_argument,NULL,8},
-        {"write-index",no_argument,NULL,10},
+        {"write-index",optional_argument,NULL,'W'},
         {NULL,0,NULL,0}
     };
     char *tmp;
-    while ((c = getopt_long(argc, argv, "l:t:T:r:R:o:O:s:S:Gf:knv:V:m:M:auUhHc:C:Ii:e:xXpPq:Q:g:",loptions,NULL)) >= 0)
+    while ((c = getopt_long(argc, argv, "l:t:T:r:R:o:O:s:S:Gf:knv:V:m:M:aAuUhHc:C:Ii:e:xXpPq:Q:g:W::",loptions,NULL)) >= 0)
     {
         char allele_type[9] = "nref";
         switch (c)
@@ -648,6 +665,7 @@ int main_vcfview(int argc, char *argv[])
             case 'S': args->sample_names = optarg; args->sample_is_file = 1; break;
             case  1 : args->force_samples = 1; break;
             case 'a': args->trim_alts = 1; args->calc_ac = 1; break;
+            case 'A': args->trim_star_allele++; break;
             case 'I': args->update_info = 0; break;
             case 'G': args->sites_only = 1; break;
 
@@ -734,7 +752,10 @@ int main_vcfview(int argc, char *argv[])
                 break;
             case  9 : args->n_threads = strtol(optarg, 0, 0); break;
             case  8 : args->record_cmd_line = 0; break;
-            case 10 : args->write_index = 1; break;
+            case 'W':
+                if (!(args->write_index = write_index_parse(optarg)))
+                    error("Unsupported index format '%s'\n", optarg);
+                break;
             case '?': usage(args); break;
             default: error("Unknown argument: %s\n", optarg);
         }
@@ -791,7 +812,9 @@ int main_vcfview(int argc, char *argv[])
     else if ( args->output_type & FT_BCF )
         error("BCF output requires header, cannot proceed with -H\n");
 
-    if ( args->write_index && init_index(args->out,out_hdr,args->fn_out,&args->index_fn)<0 ) error("Error: failed to initialise index for %s\n",args->fn_out);
+    if ( init_index2(args->out,out_hdr,args->fn_out, &args->index_fn,
+                     args->write_index) < 0 )
+        error("Error: failed to initialise index for %s\n",args->fn_out);
 
     int ret = 0;
     if (!args->header_only)
index 38417a78b91db6c7050f3488416389e47147ff08..2defb4fbb25bb85a84452a118240674d292019ed 100644 (file)
@@ -1,6 +1,6 @@
 /*  version.c -- report version numbers for plugins.
 
-    Copyright (C) 2014-2023 Genome Research Ltd.
+    Copyright (C) 2014-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -88,7 +88,8 @@ void set_wmode(char dst[8], int file_type, const char *fname, int clevel)
     const char *end = fname ? strstr(fname, HTS_IDX_DELIM) : NULL;
     if ( !end ) end = fname ? fname + strlen(fname) : fname;
     int len = end - fname;
-    if ( len >= 4 && !strncasecmp(".bcf",fname+len-4,4) ) ret = hts_bcf_wmode(FT_BCF|FT_GZ);
+    if ( len >= 4 && !strncasecmp(".bcf",fname+len-4,4) )
+        ret = hts_bcf_wmode(file_type & FT_BCF ? file_type : FT_BCF|FT_GZ);
     else if ( len >= 4 && !strncasecmp(".vcf",fname+len-4,4) ) ret = hts_bcf_wmode(FT_VCF);
     else if ( len >= 7 && !strncasecmp(".vcf.gz",fname+len-7,7) ) ret = hts_bcf_wmode(FT_VCF|FT_GZ);
     else if ( len >= 8 && !strncasecmp(".vcf.bgz",fname+len-8,8) ) ret = hts_bcf_wmode(FT_VCF|FT_GZ);
@@ -112,27 +113,55 @@ int parse_overlap_option(const char *arg)
     else return -1;
 }
 
+// Used to set args->write_index in CLI.
+// It will be true if set correctly.
+// Note due to HTS_FMT_CSI being zero we have to use an additional bit.
+int write_index_parse(char *arg) {
+    int fmt = HTS_FMT_CSI;
+
+    if (arg) {
+        if (strcmp(arg, "csi") == 0 || strcmp(arg, "=csi") == 0)
+            fmt = HTS_FMT_CSI;
+        else if (strcmp(arg, "tbi") == 0 || strcmp(arg, "=tbi") == 0)
+            fmt = HTS_FMT_TBI;
+        else
+            return 0;
+    }
+
+    return 128 | fmt;
+}
+
 // See also samtools/sam_utils.c auto_index()
-int init_index(htsFile *fh, bcf_hdr_t *hdr, char *fname, char **idx_fname)
-{
-    int min_shift = 14; // CSI
+int init_index2(htsFile *fh, bcf_hdr_t *hdr, const char *fname,
+                char **idx_fname, int idx_fmt) {
+    // Nothing to do == success.  This simplifies the main code simpler.
+    if (!idx_fmt)
+        return 0;
+
+    int min_shift;
+    char *idx_suffix;
+
+    if (idx_fmt && (idx_fmt&127) == HTS_FMT_TBI && fh->format.format == vcf) {
+        min_shift = 0;  // TBI
+        idx_suffix = "tbi";
+    } else {
+        min_shift = 14; // CSI
+        idx_suffix = "csi";
+    }
 
     if ( !fname || !*fname || !strcmp(fname, "-") ) return -1;
 
     char *delim = strstr(fname, HTS_IDX_DELIM);
-    if (delim)
-    {
+    if (delim) {
         delim += strlen(HTS_IDX_DELIM);
         *idx_fname = strdup(delim);
         if ( !*idx_fname ) return -1;
 
         size_t l = strlen(*idx_fname);
         if ( l >= 4 && strcmp(*idx_fname + l - 4, ".tbi")==0 ) min_shift = 0;
-    }
-    else
-    {
+    } else {
         if ( !(*idx_fname = malloc(strlen(fname)+6)) ) return -1;
-        sprintf(*idx_fname, "%s.csi", fname);
+        sprintf(*idx_fname, "%s.%s", fname, idx_suffix);
     }
 
     if ( bcf_idx_init(fh, hdr, min_shift, *idx_fname) < 0 ) return -1;
@@ -140,4 +169,7 @@ int init_index(htsFile *fh, bcf_hdr_t *hdr, char *fname, char **idx_fname)
     return 0;
 }
 
-
+int init_index(htsFile *fh, bcf_hdr_t *hdr, const char *fname, char **idx_fname)
+{
+    return init_index2(fh,hdr, fname, idx_fname, HTS_FMT_CSI);
+}
index 23949bf0236b816a36ab861857b587906cd44d38..4944b57e90891edb43e1ce9a4585f97fe1b8ad24 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  version.c -- report version numbers for plugins.
 
-    Copyright (C) 2014-2023 Genome Research Ltd.
+    Copyright (C) 2014-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
 
@@ -90,7 +90,8 @@ void set_wmode(char dst[8], int file_type, const char *fname, int clevel)
     const char *end = fname ? strstr(fname, HTS_IDX_DELIM) : NULL;
     if ( !end ) end = fname ? fname + strlen(fname) : fname;
     int len = end - fname;
-    if ( len >= 4 && !strncasecmp(".bcf",fname+len-4,4) ) ret = hts_bcf_wmode(FT_BCF|FT_GZ);
+    if ( len >= 4 && !strncasecmp(".bcf",fname+len-4,4) )
+        ret = hts_bcf_wmode(file_type & FT_BCF ? file_type : FT_BCF|FT_GZ);
     else if ( len >= 4 && !strncasecmp(".vcf",fname+len-4,4) ) ret = hts_bcf_wmode(FT_VCF);
     else if ( len >= 7 && !strncasecmp(".vcf.gz",fname+len-7,7) ) ret = hts_bcf_wmode(FT_VCF|FT_GZ);
     else if ( len >= 8 && !strncasecmp(".vcf.bgz",fname+len-8,8) ) ret = hts_bcf_wmode(FT_VCF|FT_GZ);
@@ -114,27 +115,55 @@ int parse_overlap_option(const char *arg)
     else return -1;
 }
 
+// Used to set args->write_index in CLI.
+// It will be true if set correctly.
+// Note due to HTS_FMT_CSI being zero we have to use an additional bit.
+int write_index_parse(char *arg) {
+    int fmt = HTS_FMT_CSI;
+
+    if (arg) {
+        if (strcmp(arg, "csi") == 0 || strcmp(arg, "=csi") == 0)
+            fmt = HTS_FMT_CSI;
+        else if (strcmp(arg, "tbi") == 0 || strcmp(arg, "=tbi") == 0)
+            fmt = HTS_FMT_TBI;
+        else
+            return 0;
+    }
+
+    return 128 | fmt;
+}
+
 // See also samtools/sam_utils.c auto_index()
-int init_index(htsFile *fh, bcf_hdr_t *hdr, char *fname, char **idx_fname)
-{
-    int min_shift = 14; // CSI
+int init_index2(htsFile *fh, bcf_hdr_t *hdr, const char *fname,
+                char **idx_fname, int idx_fmt) {
+    // Nothing to do == success.  This simplifies the main code simpler.
+    if (!idx_fmt)
+        return 0;
+
+    int min_shift;
+    char *idx_suffix;
+
+    if (idx_fmt && (idx_fmt&127) == HTS_FMT_TBI && fh->format.format == vcf) {
+        min_shift = 0;  // TBI
+        idx_suffix = "tbi";
+    } else {
+        min_shift = 14; // CSI
+        idx_suffix = "csi";
+    }
 
     if ( !fname || !*fname || !strcmp(fname, "-") ) return -1;
 
     char *delim = strstr(fname, HTS_IDX_DELIM);
-    if (delim)
-    {
+    if (delim) {
         delim += strlen(HTS_IDX_DELIM);
         *idx_fname = strdup(delim);
         if ( !*idx_fname ) return -1;
 
         size_t l = strlen(*idx_fname);
         if ( l >= 4 && strcmp(*idx_fname + l - 4, ".tbi")==0 ) min_shift = 0;
-    }
-    else
-    {
+    } else {
         if ( !(*idx_fname = malloc(strlen(fname)+6)) ) return -1;
-        sprintf(*idx_fname, "%s.csi", fname);
+        sprintf(*idx_fname, "%s.%s", fname, idx_suffix);
     }
 
     if ( bcf_idx_init(fh, hdr, min_shift, *idx_fname) < 0 ) return -1;
@@ -142,4 +171,7 @@ int init_index(htsFile *fh, bcf_hdr_t *hdr, char *fname, char **idx_fname)
     return 0;
 }
 
-
+int init_index(htsFile *fh, bcf_hdr_t *hdr, const char *fname, char **idx_fname)
+{
+    return init_index2(fh,hdr, fname, idx_fname, HTS_FMT_CSI);
+}
index 69bf963de4007b158f09e2a4fe782e2e07527645..007c916a2db004f82333b00dbb7d6c0fc10e506a 100755 (executable)
@@ -24,7 +24,7 @@
 # DEALINGS IN THE SOFTWARE.
 
 # Master version, for use in tarballs or non-git source copies
-VERSION=1.18
+VERSION=1.21
 
 # If we have a git clone, then check against the current tag
 if [ -e .git ]
diff --git a/devtools/check-platform.sh b/devtools/check-platform.sh
new file mode 100755 (executable)
index 0000000..1203133
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/sh -e
+
+case $1 in
+    ubuntu-*-arm)  expected=arm ;;
+    macos-13)      expected=x86_64 ;;
+    ubuntu-*)      expected=x86_64 ;;
+    macos-*)       expected=arm ;;
+    windows-*)     expected=x86_64 ;;
+    *)
+       echo Unknown platform $1 >&2
+       exit 2
+       ;;
+esac
+
+arch=$(uname -m)
+case $arch in
+    arm*|aarch*)  actual=arm ;;
+    x86*)         actual=x86_64 ;;
+    *)
+       echo Unrecognized uname result $arch >&2
+       exit 2
+       ;;
+esac
+
+if test $actual = $expected
+then
+    echo Running on $arch as expected
+    exit 0
+else
+    echo Platform $arch is not the expected $expected >&2
+    exit 1
+fi
index c98cc4736bc5126ff647b5e0014439e3ec1a878b..224ed084f7b1d2f11874d599dc5c5f45a0b7d175 100644 (file)
@@ -2,5 +2,7 @@ channels:
   - conda-forge
   - bioconda
 dependencies:
+  - bzip2
   - cython
   - setuptools
+  - xz
index a4652f44f8184f8bb3ff6cecfe17633bb1f08e1d..2d955dea27b70894ddfde350941f6197806929d0 100644 (file)
@@ -8,6 +8,7 @@
 
 import fnmatch
 import os
+from pathlib import Path
 import re
 import itertools
 import shutil
@@ -35,8 +36,8 @@ EXCLUDE = {
         "peakfit.h",
         "polysomy.c"),
     "htslib": (
-        'htslib/tabix.c', 'htslib/bgzip.c',
-        'htslib/htsfile.c',
+        'annot-tsv.c', 'bgzip.c', 'htsfile.c', 'tabix.c',
+        'hts_probe_cc.sh',
         "samples", "test", "tests"),
 }
 
@@ -119,6 +120,7 @@ def _update_pysam_files(cf, destdir):
                 if fn == "bamtk.c":
                     lines = re.sub(r'(#include "version.h")', r'\1\n#include "samtools_config_vars.h"', lines)
                     lines = re.sub(r'(else if.*"tview")', r'//\1', lines)
+                    lines = re.sub(r'(if[ (]*fclose)', r'if (0) { //\1', lines)
 
                 outfile.write(lines)
 
@@ -152,9 +154,10 @@ if len(sys.argv) >= 1:
                              locate("version.sh", srcdir, exclude_htslib=True))
 
     if dest == "htslib":
-        # Add build files, including *.ac *.in *.mk *.m4 *.sh
+        # Add build files, including *.ac config.{guess,sub} *.in *.mk *.m4 *.sh
         mfiles = itertools.chain(mfiles, locate("Makefile", srcdir),
                                  locate("configure", srcdir),
+                                 locate("config.*", srcdir),
                                  locate("*.[aims][cnk4h]", srcdir, exclude))
 
     ncopied = 0
@@ -243,6 +246,9 @@ if len(sys.argv) >= 1:
     _update_version_file(C_VERSION[dest], version + " (pysam)", "pysam/version.h")
     _update_version_doc_file(dest, version, "README.rst")
     _update_version_doc_file(dest, version, "doc/index.rst")
+    if dest in MAIN:
+        path = Path("doc/conf.py")
+        path.write_text(re.sub(rf"doc/[\d.]*/{dest}", rf"doc/{version}/{dest}", path.read_text()))
 
     sys.exit(0)
 
index 40081eb4cd6ee669b06b55f2e79a2b48ae856258..27c389ce897f8b27b37c6a29816b3565a349dd24 100644 (file)
@@ -51,20 +51,19 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'pysam'
-copyright = '2009–2024 Andreas Heger, John Marshall, Kevin Jacobs, et al'
+copyright = '2009–2025 Andreas Heger, John Marshall, Kevin Jacobs, et al'
 
 # Included at the end of each rst file
 rst_epilog = '''
-.. _CGAT Training Programme: http://www.cgat.org
 .. _pysam: https://github.com/pysam-developers/pysam
-.. _samtools: http://samtools.sourceforge.net/
-.. _bcftools: https://samtools.github.io/bcftools/bcftools.html
-.. _htslib: http://www.htslib.org/
-.. _tabix: http://www.htslib.org/doc/tabix.html
-.. _Galaxy: https://main.g2.bx.psu.edu/
+.. _samtools: https://www.htslib.org/doc/1.21/samtools.html
+.. _bcftools: https://www.htslib.org/doc/1.21/bcftools.html
+.. _htslib: https://www.htslib.org/
+.. _tabix: https://www.htslib.org/doc/tabix.html
+.. _Galaxy: https://usegalaxy.org/
 .. _cython: https://cython.org/
 .. _python: https://www.python.org/
-.. _pypi: https://pypi.org/
+.. _PyPI: https://pypi.org/
 .. _pip: https://pip.pypa.io/
 .. _pyximport: https://github.com/cython/cython/tree/master/pyximport
 .. _conda: https://conda.io/docs/
index 5bc306603a38ef69e4e2d4d49e2528dec20d3876..cefc9eaf10882bfa6ebd95186596623657c21dab 100644 (file)
@@ -33,6 +33,24 @@ directories:
    :file:`import.py` about importing.
 
 
+Python language level
+=====================
+
+Pysam currently requires Python 3.6 as a minimum language level.
+For example, this means that the following comparatively recent
+language features and library functions are available for use:
+
+* f-strings
+* ``raise ... from None``
+* :meth:`str.startswith`, :meth:`str.endswith`, :meth:`str.rstrip`, etc
+
+However in particular the the following should not be used in
+pysam source code or infrastructure scripts:
+
+* :meth:`str.removeprefix`, :meth:`str.removesuffix` (new in 3.9)
+* walrus ``:=`` operator (new in 3.8)
+
+
 Importing new versions of htslib and samtools
 =============================================
 
index e2352ebf02994e02a7d6bda30bdf9b3a60734d61..37cd7d43153efb8a76bfa6ee4f1c24db6c3d9e3d 100644 (file)
@@ -121,20 +121,24 @@ Thus, ``multiple_iterators`` is set to ``False`` by default.
 AlignmentFile.fetch does not show unmapped reads
 ================================================
 
-:meth:`~pysam.AlignmentFile.fetch` will only iterate over alignments
-in the SAM/BAM file. The following thus always works::
+By default, :meth:`~pysam.AlignmentFile.fetch` will only iterate over
+placed alignments in the SAM/BAM/CRAM file. Thus the following always
+works::
 
-    bf = pysam.AlignmentFile(fname, "rb")
-    for r in bf.fetch():
-        assert not r.is_unmapped
+    f = pysam.AlignmentFile(fname, "r")
+    for r in f.fetch():
+        assert r.reference_name is not None
 
-If the SAM/BAM file contains unaligned reads, they can be included
+If the file contains unaligned reads, they can be included
 in the iteration by adding the ``until_eof=True`` flag::
 
-    bf = pysam.AlignmentFile(fname, "rb")
-    for r in bf.fetch(until_eof=True):
+    f = pysam.AlignmentFile(fname, "r")
+    for r in f.fetch(until_eof=True):
         if r.is_unmapped:
-           print("read is unmapped")
+            print("read is unmapped")
+
+See also :meth:`fetch("*") <pysam.AlignmentFile.fetch>` which iterates
+only over the unplaced unmapped reads at the end of the file.
 
 I can't call AlignmentFile.fetch on a file without an index
 ===========================================================
index 0b4485ce8f6a331d4adf3550d6a6fd360b90999b..17d3d3089745b2cb5a3a594406971da8ffe26207 100644 (file)
@@ -18,7 +18,7 @@ This module provides a low-level wrapper around the htslib_ C-API as
 using cython and a high-level, pythonic API for convenient access to
 the data within genomic file formats. 
 
-The current version wraps *htslib-1.18*, *samtools-1.18*, and *bcftools-1.18*.
+The current version wraps *htslib-1.21*, *samtools-1.21*, and *bcftools-1.21*.
 
 To install the latest release, type::
 
index a659f9da697177b5802b55f1b81e299d57974869..6282fb5d78864b1aa8f595b613e431b92bc2131f 100644 (file)
@@ -4,7 +4,7 @@
 Installing pysam
 ================
 
-Pysam can be installed through conda_, pypi_ and from the repository.
+Pysam can be installed through conda_, PyPI_ and from the repository.
 The recommended way to install pysam is through conda/bioconda.
 
 Conda installation
@@ -21,7 +21,7 @@ makes sure that dependencies are installed. Also, compilation flags
 will be set automatically, which will potentially save a lot of
 trouble on OS X.
 
-Pypi installation
+PyPI installation
 =================
 
 Pysam provides a python interface to the functionality contained
@@ -31,7 +31,7 @@ can be combined, ``builtin`` and ``external``.
 Builtin
 -------
 
-The typical installation will be through pypi_::
+The typical installation will be through PyPI_::
 
    pip install pysam
 
@@ -89,7 +89,7 @@ To install from repository, type::
 
     python setup.py install
 
-For compilation options, see the section on Pypi installation above.
+For compilation options, see the section on PyPI installation above.
 
 Requirements
 ============
index 3c8ab04e15bcb42b12b7e8a69f8ea3fac205ec8d..df4a2255edc23f566be24ba06cef2214ef2f3484 100644 (file)
@@ -119,23 +119,33 @@ formatted file on stdout::
 
 Note that the file open mode needs to changed from ``r`` to ``rb``.
 
-=====================================
-Using samtools commands within python
-=====================================
+==================================================
+Using samtools and bcftools commands within Python
+==================================================
 
-Commands available in `samtools`_ are available as simple
-function calls. Command line options are provided as arguments. For
+Commands available in `samtools`_ and `bcftools`_ are available as simple
+function calls, with command line options provided as arguments. For
 example::
 
-   pysam.sort("-o", "output.bam", "ex1.bam")
+   import pysam.samtools
+   pysam.samtools.sort("-o", "output.bam", "ex1.bam", catch_stdout=False)
 
-corresponds to the command line::
+   import pysam.bcftools
+   pysam.bcftools.index("--csi", "ex2.vcf.gz")
+
+corresponds to the command lines::
 
    samtools sort -o output.bam ex1.bam
+   bcftools index --csi ex2.vcf.gz
+
+Samtools commands are also imported into the main ``pysam`` namespace.
+For example::
 
-Or for example::
+   pysam.sort("-m", "1000000", "-o", "output.bam", "ex1.bam", catch_stdout=False)
 
-   pysam.sort("-m", "1000000", "-o", "output.bam", "ex1.bam")
+To make them valid Python identifiers, the functions :func:`!cram_size`
+and :func:`!fqimport` are spelt thus, differently from their
+corresponding commands.
 
 In order to get usage information, try::
 
@@ -153,24 +163,63 @@ Argument errors raise a :class:`pysam.SamtoolsError`::
    pysam.SamtoolsError: 'Usage: samtools sort [-n] [-m <maxMem>] <in.bam> <out.prefix>\n'
 
 Messages from `samtools`_ on stderr are captured and are
-available using the :meth:`getMessages` method::
+available using the :meth:`~PysamDispatcher.get_messages` method::
+
+   pysam.sort.get_messages()
+
+By default, pysam captures the samtools command's standard output and returns it
+as the function's return value. To redirect stdout to a file instead, either use
+the ``save_stdout`` keyword argument, or use ``"-o", "filename"`` in the arguments
+and also use ``catch_stdout=False`` to prevent pysam's capturing from overriding
+your redirection. Finally, ``catch_stdout=False`` by itself discards standard output,
+which may help resolve problems in environments such as IPython notebooks::
+
+   # Return value
+   pileup_text = pysam.samtools.mpileup("in.bam")
+
+   # Save to file
+   pysam.samtools.mpileup("in.bam", save_stdout=pileup_filename)
+   pysam.samtools.mpileup("-o", pileup_filename, "in.bam", catch_stdout=False)
+
+   # Discard standard output
+   pysam.samtools.mpileup("in.bam", catch_stdout=False)  # Returns None
+
+For each :obj:`!command` available as a `samtools`_ subcommand,
+the following functions are provided:
+
+.. py:function:: pysam.samtools.command(args, *, catch_stdout=True, save_stdout=None, split_lines=False)
+
+   :param args: Arguments to be passed to the samtools subcommand.
+   :param bool catch_stdout: Whether to return stdout as the function's value.
+   :param str save_stdout: Filename to which stdout should be written.
+   :param bool split_lines: Whether to split the return value into a list of lines.
+   :returns: Standard output if it was caught, otherwise None.
+
+   If `save_stdout` is not None, the command's standard ouput is written to the
+   file specified and the function returns None.
+
+   Otherwise, if `catch_stdout` is true, the command's standard output is captured
+   and used as the function's return value --- either as a single :obj:`str` or as
+   :obj:`list[str] <list>` according to `split_lines`. If `catch_stdout` is false,
+   the command's standard output is discarded and the function returns None.
+
+   The command's standard error is always captured and made available via
+   :func:`~pysam.samtools.command.get_messages`.
+
+.. py:function:: pysam.samtools.command.get_messages()
 
-   pysam.sort.getMessage()
+   Returns the standard error from the most recent invocation of the particular
+   :obj:`!command`, either as a single :obj:`str` or as :obj:`list[str] <list>`
+   according to `split_lines` as specified in that invocation.
 
-Note that only the output from the last invocation of a command is
-stored.
+.. py:function:: pysam.samtools.command.usage()
 
-In order for pysam to make the output of samtools commands accessible
-the stdout stream needs to be redirected. This is the default
-behaviour, but can cause problems in environments such as the ipython
-notebook. A solution is to pass the ``catch_stdout`` keyword
-argument::
+   Returns the command's usage/help message, as a single :obj:`str`.
 
-   pysam.sort(catch_stdout=False)
+For each :obj:`!command` available as a `bcftools`_ subcommand, the
+:func:`!pysam.bcftools.command`, :func:`!pysam.bcftools.command.get_messages`,
+and :func:`!pysam.bcftools.command.usage` functions operate similarly.
 
-Note that this means that output from commands which produce output on
-stdout will not be available. The only solution is to run samtools
-commands through subprocess.
 
 ================================
 Working with tabix-indexed files
index 7f3c566829abcda4fa06f427363c25bff2898743..ed539e01644b75a4bbd420eceb4fd85aafd6abc6 100644 (file)
@@ -1,65 +1,32 @@
-try:
-    from typing import Final
-    HAVE_FINAL = True
-except ImportError:
-    HAVE_FINAL = False
+import pysam.utils
 
-from pysam.utils import PysamDispatcher
+annotate = pysam.utils.PysamDispatcher('bcftools', 'annotate')
+call = pysam.utils.PysamDispatcher('bcftools', 'call')
+cnv = pysam.utils.PysamDispatcher('bcftools', 'cnv')
+concat = pysam.utils.PysamDispatcher('bcftools', 'concat')
+consensus = pysam.utils.PysamDispatcher('bcftools', 'consensus')
+convert = pysam.utils.PysamDispatcher('bcftools', 'convert')
+csq = pysam.utils.PysamDispatcher('bcftools', 'csq')
+filter = pysam.utils.PysamDispatcher('bcftools', 'filter')
+gtcheck = pysam.utils.PysamDispatcher('bcftools', 'gtcheck')
+head = pysam.utils.PysamDispatcher('bcftools', 'head')
+index = pysam.utils.PysamDispatcher('bcftools', 'index')
+isec = pysam.utils.PysamDispatcher('bcftools', 'isec')
+merge = pysam.utils.PysamDispatcher('bcftools', 'merge')
+mpileup = pysam.utils.PysamDispatcher('bcftools', 'mpileup')
+norm = pysam.utils.PysamDispatcher('bcftools', 'norm')
+plugin = pysam.utils.PysamDispatcher('bcftools', 'plugin')
+query = pysam.utils.PysamDispatcher('bcftools', 'query')
+reheader = pysam.utils.PysamDispatcher('bcftools', 'reheader')
+roh = pysam.utils.PysamDispatcher('bcftools', 'roh')
+sort = pysam.utils.PysamDispatcher('bcftools', 'sort')
+stats = pysam.utils.PysamDispatcher('bcftools', 'stats')
+view = pysam.utils.PysamDispatcher('bcftools', 'view')
 
-_BCFTOOLS_DISPATCH = [
-    "index",
-    "annotate",
-    "concat",
-    "convert",
-    "isec",
-    "merge",
-    "norm",
-    "plugin",
-    "query",
-    "reheader",
-    "sort",
-    "view",
-    "head",
-    "call",
-    "consensus",
-    "cnv",
-    "csq",
-    "filter",
-    "gtcheck",
-    "mpileup",
-    "roh",
-    "stats"]
-
-
-def _wrap_command(dispatch: str) -> PysamDispatcher:
-    return PysamDispatcher("bcftools", dispatch, ())
-
-
-if not HAVE_FINAL:
-    # instantiate bcftools commands as python functions
-    for cmd in _BCFTOOLS_DISPATCH:
-        globals()[cmd] = PysamDispatcher("bcftools", cmd, None)
-else:
-    # python >=3.8
-    index: Final[PysamDispatcher] = _wrap_command("index")
-    annotate: Final[PysamDispatcher] = _wrap_command("annotate")
-    concat: Final[PysamDispatcher] = _wrap_command("concat")
-    convert: Final[PysamDispatcher] = _wrap_command("convert")
-    isec: Final[PysamDispatcher] = _wrap_command("isec")
-    merge: Final[PysamDispatcher] = _wrap_command("merge")
-    norm: Final[PysamDispatcher] = _wrap_command("norm")
-    plugin: Final[PysamDispatcher] = _wrap_command("plugin")
-    query: Final[PysamDispatcher] = _wrap_command("query")
-    reheader: Final[PysamDispatcher] = _wrap_command("reheader")
-    sort: Final[PysamDispatcher] = _wrap_command("sort")
-    view: Final[PysamDispatcher] = _wrap_command("view")
-    head: Final[PysamDispatcher] = _wrap_command("head")
-    call: Final[PysamDispatcher] = _wrap_command("call")
-    consensus: Final[PysamDispatcher] = _wrap_command("consensus")
-    cnv: Final[PysamDispatcher] = _wrap_command("cnv")
-    csq: Final[PysamDispatcher] = _wrap_command("csq")
-    filter: Final[PysamDispatcher] = _wrap_command("filter")
-    gtcheck: Final[PysamDispatcher] = _wrap_command("gtcheck")
-    mpileup: Final[PysamDispatcher] = _wrap_command("mpileup")
-    roh: Final[PysamDispatcher] = _wrap_command("roh")
-    stats: Final[PysamDispatcher] = _wrap_command("stats")
+__all__ = [
+    'annotate', 'call', 'cnv', 'concat', 'consensus',
+    'convert', 'csq', 'filter', 'gtcheck', 'head',
+    'index', 'isec', 'merge', 'mpileup', 'norm',
+    'plugin', 'query', 'reheader', 'roh', 'sort',
+    'stats', 'view',
+]
index bea806e72582682bab804d122c9ad784f5c3e01e..fdd142abe382520265e369e5693f23fc730dc183 100644 (file)
@@ -43,30 +43,30 @@ KEY_NAMES: List[str]
 TagValue = Union[str, int, float, array]
 
 class CIGAR_OPS(enum.IntEnum):
-    CBACK: int
-    CDEL: int
-    CDIFF: int
-    CEQUAL: int
-    CHARD_CLIP: int
-    CINS: int
-    CMATCH: int
-    CPAD: int
-    CREF_SKIP: int
-    CSOFT_CLIP: int
+    CBACK = ...
+    CDEL = ...
+    CDIFF = ...
+    CEQUAL = ...
+    CHARD_CLIP = ...
+    CINS = ...
+    CMATCH = ...
+    CPAD = ...
+    CREF_SKIP = ...
+    CSOFT_CLIP = ...
 
 class SAM_FLAGS(enum.IntEnum):
-    FDUP: int
-    FMREVERSE: int
-    FMUNMAP: int
-    FPAIRED: int
-    FPROPER_PAIR: int
-    FQCFAIL: int
-    FREAD1: int
-    FREAD2: int
-    FREVERSE: int
-    FSECONDARY: int
-    FSUPPLEMENTARY: int
-    FUNMAP: int
+    FDUP = ...
+    FMREVERSE = ...
+    FMUNMAP = ...
+    FPAIRED = ...
+    FPROPER_PAIR = ...
+    FQCFAIL = ...
+    FREAD1 = ...
+    FREAD2 = ...
+    FREVERSE = ...
+    FSECONDARY = ...
+    FSUPPLEMENTARY = ...
+    FUNMAP = ...
 
 class AlignedSegment:
     header: AlignmentHeader
@@ -136,7 +136,7 @@ class AlignedSegment:
     def get_forward_sequence(self) -> Optional[str]: ...
     def get_forward_qualities(self) -> Optional[array]: ...
     def get_aligned_pairs(
-        self, matches_only: bool = ..., with_seq: bool = ...
+        self, matches_only: bool = ..., with_seq: bool = ..., with_cigar: bool = ...
     ) -> List[Tuple[int, int]]: ...
     def get_blocks(self) -> List[Tuple[int, int]]: ...
     def get_overlap(self, start: int, end: int) -> Optional[int]: ...
index 3071f3753ad0e00c78022b28c944a3974cccb511..c9c27734536193a42643373cbcdfcb0a60317aed 100644 (file)
@@ -382,7 +382,7 @@ cdef inline pack_tags(tags):
                     raise ValueError("unsupported type code '{}'".format(value.typecode))
 
             if typecode not in DATATYPE2FORMAT:
-                raise ValueError("invalid value type '{}' ({})".format(chr(typecode), array.typecode))
+                raise ValueError("invalid value type '{}'".format(chr(typecode)))
 
             # use array.tostring() to retrieve byte representation and
             # save as bytes
@@ -407,8 +407,10 @@ cdef inline pack_tags(tags):
 
             if typecode == b'Z' or typecode == b'H':
                 datafmt = "2sB%is" % (len(value)+1)
-            else:
+            elif typecode in DATATYPE2FORMAT:
                 datafmt = "2sB%s" % DATATYPE2FORMAT[typecode][0]
+            else:
+                raise ValueError("invalid value type '{}'".format(chr(typecode)))
 
             args.extend([pytag[:2],
                          typecode,
@@ -757,7 +759,8 @@ cdef inline bytes build_alignment_sequence(bam1_t * src):
         elif op == BAM_CHARD_CLIP:
             pass # advances neither
 
-    cdef char *md_tag, md_buffer[2];
+    cdef char md_buffer[2]
+    cdef char *md_tag
     cdef uint8_t md_typecode = md_tag_ptr[0]
     if md_typecode == b'Z':
         md_tag = bam_aux2Z(md_tag_ptr)
@@ -891,6 +894,32 @@ cdef inline bytes build_reference_sequence(bam1_t * src):
     return seq
 
 
+cdef inline str safe_reference_name(AlignmentHeader header, int tid):
+    if tid == -1: return "*"
+    elif header is not None: return header.get_reference_name(tid)
+    else: return f"#{tid}"
+
+
+# Tuple-building helper functions used by AlignedSegment.get_aligned_pairs()
+
+cdef _alignedpairs_positions(qpos, pos, ref_seq, uint32_t r_idx, int op):
+    return (qpos, pos)
+
+
+cdef _alignedpairs_with_seq(qpos, pos, ref_seq, uint32_t r_idx, int op):
+    ref_base = ref_seq[r_idx] if ref_seq is not None else None
+    return (qpos, pos, ref_base)
+
+
+cdef _alignedpairs_with_cigar(qpos, pos, ref_seq, uint32_t r_idx, int op):
+    return (qpos, pos, CIGAR_OPS(op))
+
+
+cdef _alignedpairs_with_seq_cigar(qpos, pos, ref_seq, uint32_t r_idx, int op):
+    ref_base = ref_seq[r_idx] if ref_seq is not None else None
+    return (qpos, pos, ref_base, CIGAR_OPS(op))
+
+
 cdef class AlignedSegment:
     '''Class representing an aligned segment.
 
@@ -957,7 +986,8 @@ cdef class AlignedSegment:
 
         The representation is an approximate :term:`SAM` format, because
         an aligned read might not be associated with a :term:`AlignmentFile`.
-        As a result :term:`tid` is shown instead of the reference name.
+        Hence when the read does not have an associated :class:`AlignedHeader`,
+        :term:`tid` is shown instead of the reference name.
         Similarly, the tags field is returned in its parsed state.
 
         To get a valid SAM record, use :meth:`to_string`.
@@ -966,17 +996,21 @@ cdef class AlignedSegment:
         # requires a valid header.
         return "\t".join(map(str, (self.query_name,
                                    self.flag,
-                                   "#%d" % self.reference_id if self.reference_id >= 0 else "*",
+                                   safe_reference_name(self.header, self.reference_id),
                                    self.reference_start + 1,
                                    self.mapping_quality,
                                    self.cigarstring,
-                                   "#%d" % self.next_reference_id if self.next_reference_id >= 0 else "*",
+                                   safe_reference_name(self.header, self.next_reference_id),
                                    self.next_reference_start + 1,
                                    self.template_length,
                                    self.query_sequence,
                                    self.query_qualities,
                                    self.tags)))
 
+    def __repr__(self):
+        ref = self.reference_name if self.header is not None else self.reference_id
+        return f'<{type(self).__name__}({self.query_name!r}, flags={self.flag}={self.flag:#x}, ref={ref!r}, zpos={self.reference_start}, mapq={self.mapping_quality}, cigar={self.cigarstring!r}, ...)>'
+
     def __copy__(self):
         return makeAlignedSegment(self._delegate, self.header)
 
@@ -1972,8 +2006,7 @@ cdef class AlignedSegment:
         else:
             return self.query_qualities
 
-
-    def get_aligned_pairs(self, matches_only=False, with_seq=False):
+    def get_aligned_pairs(self, matches_only=False, with_seq=False, with_cigar=False):
         """a list of aligned read (query) and reference positions.
 
         Each item in the returned list is a tuple consisting of
@@ -1997,6 +2030,9 @@ cdef class AlignedSegment:
           reference sequence. For CIGAR 'P' (padding in the reference)
           operations, the third tuple element will be None. Substitutions
           are lower-case. This option requires an MD tag to be present.
+        with_cigar : bool
+          If True, return an extra element in the tuple containing the
+          CIGAR operator corresponding to this position tuple.
 
         Returns
         -------
@@ -2010,6 +2046,8 @@ cdef class AlignedSegment:
         cdef bam1_t * src = self._delegate
         cdef bint _matches_only = bool(matches_only)
         cdef bint _with_seq = bool(with_seq)
+        cdef bint _with_cigar = bool(with_cigar)
+        cdef object (*make_tuple)(object, object, object, uint32_t, int)
 
         # TODO: this method performs no checking and assumes that
         # read sequence, cigar and MD tag are consistent.
@@ -2019,6 +2057,10 @@ cdef class AlignedSegment:
             ref_seq = force_str(build_reference_sequence(src))
             if ref_seq is None:
                 raise ValueError("MD tag not present")
+            make_tuple = _alignedpairs_with_seq_cigar if _with_cigar else _alignedpairs_with_seq
+        else:
+            ref_seq = None
+            make_tuple = _alignedpairs_with_cigar if _with_cigar else _alignedpairs_positions
 
         r_idx = 0
 
@@ -2034,39 +2076,25 @@ cdef class AlignedSegment:
             l = cigar_p[k] >> BAM_CIGAR_SHIFT
 
             if op == BAM_CMATCH or op == BAM_CEQUAL or op == BAM_CDIFF:
-                if _with_seq:
-                    for i from pos <= i < pos + l:
-                        result.append((qpos, i, ref_seq[r_idx]))
-                        r_idx += 1
-                        qpos += 1
-                else:
-                    for i from pos <= i < pos + l:
-                        result.append((qpos, i))
-                        qpos += 1
+                for i from pos <= i < pos + l:
+                    result.append(make_tuple(qpos, i, ref_seq, r_idx, op))
+                    r_idx += 1
+                    qpos += 1
                 pos += l
 
             elif op == BAM_CINS or op == BAM_CSOFT_CLIP or op == BAM_CPAD:
                 if not _matches_only:
-                    if _with_seq:
-                        for i from pos <= i < pos + l:
-                            result.append((qpos, None, None))
-                            qpos += 1
-                    else:
-                        for i from pos <= i < pos + l:
-                            result.append((qpos, None))
-                            qpos += 1
+                    for i from pos <= i < pos + l:
+                        result.append(make_tuple(qpos, None, None, 0, op))
+                        qpos += 1
                 else:
                     qpos += l
 
             elif op == BAM_CDEL:
                 if not _matches_only:
-                    if _with_seq:
-                        for i from pos <= i < pos + l:
-                            result.append((None, i, ref_seq[r_idx]))
-                            r_idx += 1
-                    else:
-                        for i from pos <= i < pos + l:
-                            result.append((None, i))
+                    for i from pos <= i < pos + l:
+                        result.append(make_tuple(None, i, ref_seq, r_idx, op))
+                        r_idx += 1
                 else:
                     r_idx += l
                 pos += l
@@ -2076,12 +2104,8 @@ cdef class AlignedSegment:
 
             elif op == BAM_CREF_SKIP:
                 if not _matches_only:
-                    if _with_seq:
-                        for i from pos <= i < pos + l:
-                            result.append((None, i, None))
-                    else:
-                        for i from pos <= i < pos + l:
-                            result.append((None, i))
+                    for i from pos <= i < pos + l:
+                        result.append(make_tuple(None, i, None, 0, op))
 
                 pos += l
 
@@ -2163,31 +2187,32 @@ cdef class AlignedSegment:
 
         The output order in the array is "MIDNSHP=X" followed by a
         field for the NM tag. If the NM tag is not present, this
-        field will always be 0.
-
-        +-----+--------------+-----+
-        |M    |BAM_CMATCH    |0    |
-        +-----+--------------+-----+
-        |I    |BAM_CINS      |1    |
-        +-----+--------------+-----+
-        |D    |BAM_CDEL      |2    |
-        +-----+--------------+-----+
-        |N    |BAM_CREF_SKIP |3    |
-        +-----+--------------+-----+
-        |S    |BAM_CSOFT_CLIP|4    |
-        +-----+--------------+-----+
-        |H    |BAM_CHARD_CLIP|5    |
-        +-----+--------------+-----+
-        |P    |BAM_CPAD      |6    |
-        +-----+--------------+-----+
-        |=    |BAM_CEQUAL    |7    |
-        +-----+--------------+-----+
-        |X    |BAM_CDIFF     |8    |
-        +-----+--------------+-----+
-        |B    |BAM_CBACK     |9    |
-        +-----+--------------+-----+
-        |NM   |NM tag        |10   |
-        +-----+--------------+-----+
+        field will always be 0. (Accessing this field via index -1
+        avoids changes if more CIGAR operators are added in future.)
+
+        +-----+----------------+--------+
+        |M    |pysam.CMATCH    |0       |
+        +-----+----------------+--------+
+        |I    |pysam.CINS      |1       |
+        +-----+----------------+--------+
+        |D    |pysam.CDEL      |2       |
+        +-----+----------------+--------+
+        |N    |pysam.CREF_SKIP |3       |
+        +-----+----------------+--------+
+        |S    |pysam.CSOFT_CLIP|4       |
+        +-----+----------------+--------+
+        |H    |pysam.CHARD_CLIP|5       |
+        +-----+----------------+--------+
+        |P    |pysam.CPAD      |6       |
+        +-----+----------------+--------+
+        |=    |pysam.CEQUAL    |7       |
+        +-----+----------------+--------+
+        |X    |pysam.CDIFF     |8       |
+        +-----+----------------+--------+
+        |B    |pysam.CBACK     |9       |
+        +-----+----------------+--------+
+        |NM   |NM tag          |10 or -1|
+        +-----+----------------+--------+
 
         If no cigar string is present, empty arrays will be returned.
 
@@ -2242,27 +2267,27 @@ cdef class AlignedSegment:
 
         The operations are:
 
-        +-----+--------------+-----+
-        |M    |BAM_CMATCH    |0    |
-        +-----+--------------+-----+
-        |I    |BAM_CINS      |1    |
-        +-----+--------------+-----+
-        |D    |BAM_CDEL      |2    |
-        +-----+--------------+-----+
-        |N    |BAM_CREF_SKIP |3    |
-        +-----+--------------+-----+
-        |S    |BAM_CSOFT_CLIP|4    |
-        +-----+--------------+-----+
-        |H    |BAM_CHARD_CLIP|5    |
-        +-----+--------------+-----+
-        |P    |BAM_CPAD      |6    |
-        +-----+--------------+-----+
-        |=    |BAM_CEQUAL    |7    |
-        +-----+--------------+-----+
-        |X    |BAM_CDIFF     |8    |
-        +-----+--------------+-----+
-        |B    |BAM_CBACK     |9    |
-        +-----+--------------+-----+
+        +-----+----------------+-----+
+        |M    |pysam.CMATCH    |0    |
+        +-----+----------------+-----+
+        |I    |pysam.CINS      |1    |
+        +-----+----------------+-----+
+        |D    |pysam.CDEL      |2    |
+        +-----+----------------+-----+
+        |N    |pysam.CREF_SKIP |3    |
+        +-----+----------------+-----+
+        |S    |pysam.CSOFT_CLIP|4    |
+        +-----+----------------+-----+
+        |H    |pysam.CHARD_CLIP|5    |
+        +-----+----------------+-----+
+        |P    |pysam.CPAD      |6    |
+        +-----+----------------+-----+
+        |=    |pysam.CEQUAL    |7    |
+        +-----+----------------+-----+
+        |X    |pysam.CDIFF     |8    |
+        +-----+----------------+-----+
+        |B    |pysam.CBACK     |9    |
+        +-----+----------------+-----+
 
         .. note::
             The output is a list of (operation, length) tuples, such as
index 28b395aed078cb76af6ef33e3e5df0582ea3673d..6f106af9b7bd6d54fdbad4733c9b46f3e623e6ce 100644 (file)
@@ -20,7 +20,7 @@ else:
     from typing import Literal
 
 from pysam.libchtslib import HTSFile, _HasFileNo
-from pysam.libcalignedsegment import AlignedSegment
+from pysam.libcalignedsegment import AlignedSegment, PileupColumn
 from pysam.libcfaidx import FastaFile
 
 class IndexStats(NamedTuple):
@@ -67,7 +67,7 @@ class AlignmentHeader:
     def lengths(self) -> Tuple[int]: ...
     def to_dict(self) -> Dict: ...
     def get_reference_name(self, tid: int) -> Optional[str]: ...
-    def get_reference_length(self, reference: int) -> int: ...
+    def get_reference_length(self, reference: str) -> int: ...
     def is_valid_tid(self, tid: int) -> bool: ...
     def get_tid(self, reference: str) -> int: ...
 
@@ -204,7 +204,7 @@ class IteratorRowRegion(IteratorRow): ...
 class IteratorRowSelection(IteratorRow): ...
 
 class IteratorColumn:
-    def __iter__(self) -> IteratorRow: ...
+    def __iter__(self) -> IteratorColumn: ...
     def __next__(self) -> PileupColumn: ...
     @property
     def seq_len(self) -> int: ...
index d62f16966fd75fc6c46e07a5faac35b12d2d6351..e643562d651dc1b223f5ae091e8d36c6be36cd34 100644 (file)
@@ -152,7 +152,7 @@ class VariantHeader:
         stop: int = ...,
         alleles: Optional[Tuple[str, ...]] = ...,
         id: Optional[str] = ...,
-        qual: Optional[int] = ...,
+        qual: Optional[float] = ...,
         filter: Optional[Any] = ...,
         info: Optional[Mapping[str, _InfoValue]] = ...,
         samples: Optional[Iterable[Optional[Mapping[str, _FormatValue]]]] = ...,
@@ -218,7 +218,7 @@ class VariantRecord:
     start: int
     stop: int
     rlen: int
-    qual: Optional[int]
+    qual: Optional[float]
     id: Optional[str]
     ref: Optional[str]
     alleles: Optional[Tuple[str, ...]]
@@ -241,8 +241,8 @@ class VariantRecordSample(_Mapping[str, _FormatValue]):
     def index(self) -> int: ...
     @property
     def name(self) -> str: ...
-    allele_indices: Optional[Tuple[Optional[int, ...]]]
-    alleles: Optional[Tuple[Optional[str, ...]]]
+    allele_indices: Optional[Tuple[Optional[int], ...]]
+    alleles: Optional[Tuple[Optional[str], ...]]
     phased: bool
     def __setitem__(self, key: str, value: _FormatValue) -> None: ...
     def __delitem__(self, key: str) -> None: ...
index 8ecfe5f3888e7d02ad5fa1400b625debc471871a..c939887dca248fe5dc8a2f54baef0e96bb4a3cd7 100644 (file)
@@ -2111,14 +2111,10 @@ cdef class VariantHeader(object):
             rec.info.update(info)
 
         if kwargs:
-            if 'GT' in kwargs:
-                rec.samples[0]['GT'] = kwargs.pop('GT')
             rec.samples[0].update(kwargs)
 
         if samples:
             for i, sample in enumerate(samples):
-                if 'GT' in sample:
-                    rec.samples[i]['GT'] = sample.pop('GT')
                 rec.samples[i].update(sample)
 
         return rec
diff --git a/pysam/libcbgzf.pxd b/pysam/libcbgzf.pxd
new file mode 100644 (file)
index 0000000..d81984d
--- /dev/null
@@ -0,0 +1,7 @@
+# cython: language_level=3
+
+from pysam.libchtslib cimport BGZF
+
+cdef class BGZFile(object):
+    cdef BGZF *bgzf
+    cdef readonly object name, index
index d66a3c6125b8e9267df1cf2e17a815ce02a40aee..9a333f4cca148f2d494868cc53856c4f88090cb9 100644 (file)
@@ -34,9 +34,6 @@ cdef class BGZFile(object):
     This class only supports opening files in binary mode. If you need to open a
     compressed file in text mode, use the gzip.open() function.
     """
-    cdef BGZF* bgzf
-    cdef readonly object name, index
-
     def __init__(self, filename, mode=None, index=None):
         """Constructor for the BGZFile class.
 
index 56e746074eb4c691b3c8ce5c54daa9bb227780c0..00075cc9fc90ead3c7005ddb4a94e62f222ac782 100644 (file)
@@ -573,6 +573,12 @@ cdef extern from "htslib/hts.h" nogil:
     # @param mode     Open mode, as per hts_open()
     htsFile *hts_hopen(hFILE *fp, const char *fn, const char *mode)
 
+    # @abstract  For output streams, flush any buffered data
+    # @param fp  The file handle to be flushed
+    # @return    0 for success, or negative if an error occurred.
+    # @since     1.14
+    int hts_flush(htsFile *fp)
+
     # @abstract  Close a file handle, flushing buffered data for output streams
     # @param fp  The file handle to be closed
     # @return    0 for success, or negative if an error occurred.
index fcd793596fbcfd2f19d962b81f1779abe9ed0d1b..61b64b6a34bf3f90bd0577581e368292348a65b9 100644 (file)
@@ -64,6 +64,7 @@ class HTSFile:
     @property
     def duplicate_filehandle(self) -> bool: ...
     def close(self) -> None: ...
+    def flush(self) -> None: ...
     def check_truncation(self, ignore_truncation: bool = ...) -> None: ...
     @property
     def category(self) -> str: ...
index 3cb7b7affa14bf08dc38007f279d3922d667dbfd..25d79ecc34ced3972b6fc56f10880a29eeec92eb 100644 (file)
@@ -338,6 +338,12 @@ cdef class HTSFile(object):
             hts_close(self.htsfile)
             self.htsfile = NULL
 
+    def flush(self):
+        """Flush any buffered data to the underlying output stream."""
+        if self.htsfile:
+            if hts_flush(self.htsfile) < 0:
+                raise OSError(errno, f'Flushing {type(self).__name__} failed', force_str(self.filename))
+
     def check_truncation(self, ignore_truncation=False):
         """Check if file is truncated."""
         if not self.htsfile:
index e1ba2113e771840467ba139431eb1472019c644d..517a74d213eb2f41a30946ab7b1c8403a7bd143d 100644 (file)
@@ -1,4 +1,10 @@
-from typing import Optional, Literal, List, Any
+import sys
+from typing import Optional, List, Any
+
+if sys.version_info < (3, 8):
+    from typing_extensions import Literal
+else:
+    from typing import Literal
 
 from pysam.libchtslib import HTSFile
 
index 64cb97ae606482272f231aec3fba184dfe9cacd1..72eaee5599a1edd84bd72949aca6dc698b64c94b 100644 (file)
@@ -12,12 +12,13 @@ from codecs import register_error
 from cpython.version cimport PY_MAJOR_VERSION, PY_MINOR_VERSION
 from cpython cimport PyBytes_Check, PyUnicode_Check
 from cpython cimport array as c_array
+from libc.errno cimport errno
 from libc.stdlib cimport calloc, free
 from libc.string cimport strncpy
 from libc.stdint cimport INT32_MAX, int32_t
 from libc.stdio cimport fprintf, stderr, fflush
 from libc.stdio cimport stdout as c_stdout
-from posix.fcntl cimport open as c_open, O_WRONLY
+from posix.fcntl cimport open as c_open, O_WRONLY, O_CREAT, O_TRUNC
 from posix.unistd cimport SEEK_SET, SEEK_CUR, SEEK_END
 
 from pysam.libcsamtools cimport samtools_dispatch, samtools_set_stdout, samtools_set_stderr, \
@@ -321,9 +322,9 @@ def _pysam_dispatch(collection,
     if save_stdout:
         stdout_f = save_stdout
         stdout_h = c_open(force_bytes(stdout_f),
-                          O_WRONLY)
+                          O_WRONLY|O_CREAT|O_TRUNC, 0666)
         if stdout_h == -1:
-            raise IOError("error while opening {} for writing".format(stdout_f))
+            raise OSError(errno, "error while opening file for writing", stdout_f)
 
         samtools_set_stdout_fn(force_bytes(stdout_f))
         bcftools_set_stdout_fn(force_bytes(stdout_f))
@@ -344,7 +345,7 @@ def _pysam_dispatch(collection,
         if collection == "bcftools":
             # in bcftools, most methods accept -o, the exceptions
             # are below:
-            if method not in ("index", "roh", "stats"):
+            if method not in ("head", "index", "roh", "stats"):
                 stdout_option = "-o {}"
         elif method in MAP_STDOUT_OPTIONS[collection]:
             # special case - samtools view -c outputs on stdout
index 046658f9542ce22423dd3f935a5584ce46bc08e5..56f6fb077e1820a5155d182fd7f1e724d51a7251 100644 (file)
-import platform
-from typing import (
-    Callable,
-    List,
-    Tuple,
-    Iterable,
-    Union,
-)
-try:
-    from typing import Final
-    HAVE_FINAL = True
-except ImportError:
-    HAVE_FINAL = False
-
-from pysam.utils import PysamDispatcher
-
-
-# samtools command line options to export in python
-_SAMTOOLS_DISPATCH = {
-    # samtools 'documented' commands
-    "view": ("view", ()),
-    "head": ("head", ()),
-    "sort": ("sort", ()),
-    "mpileup": ("mpileup", ()),
-    "consensus": ("consensus", ()),
-    "depth": ("depth", ()),
-    "faidx": ("faidx", ()),
-    "fqidx": ("fqidx", ()),
-    "tview": ("tview", ()),
-    "index": ("index", ()),
-    "idxstats": ("idxstats", ()),
-    "fixmate": ("fixmate", ()),
-    "flagstat": ("flagstat", ()),
-    "calmd": ("calmd", ()),
-    "merge": ("merge", ()),
-    "markdup": ("markdup", ()),
-    "rmdup": ("rmdup", ()),
-    "reference": ("reference", ()),
-    "reheader": ("reheader", ()),
-    "reset": ("reset", ()),
-    "cat": ("cat", ()),
-    "targetcut": ("targetcut", ()),
-    "phase": ("phase", ()),
-    "bam2fq": ("bam2fq", ()),
-    "dict": ("dict", ()),
-    "addreplacerg": ("addreplacerg", ()),
-    "pad2unpad": ("pad2unpad", ()),
-    "depad": ("pad2unpad", ()),
-    "bedcov": ("bedcov", ()),
-    "coverage": ("coverage", ()),
-    "bamshuf": ("bamshuf", ()),
-    "collate": ("collate", ()),
-    "stats": ("stats", ()),
-    "fasta": ("fasta", ()),
-    "fastq": ("fastq", ()),
-    "cram_size": ("cram-size", ()),
-    "quickcheck": ("quickcheck", ()),
-    "split": ("split", ()),
-    "flags": ("flags", ()),
-    "ampliconclip": ("ampliconclip", ()),
-    "ampliconstats": ("ampliconstats", ()),
-    "version": ("version", ()),
-    "fqimport": ("import", ()),
-    "import_": ("import", ()),
-    "samples": ("samples", ()),
-}
-
-
-def _wrap_command(
-    dispatch: str,
-    parsers: Iterable[Tuple[str, Callable[[Union[str, List[str]]], Union[str, List[str]]]]],
-) -> PysamDispatcher:
-    return PysamDispatcher("samtools", dispatch, parsers)
-
-
-if not HAVE_FINAL:
-    # python 3.7
-    for key, options in _SAMTOOLS_DISPATCH.items():
-        cmd, parser = options
-        globals()[key] = PysamDispatcher("samtools", cmd, parser)
-
-    __all__ = list(_SAMTOOLS_DISPATCH)
-else:
-    # python >=3.8
-    view: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["view"][0], _SAMTOOLS_DISPATCH["view"][1])
-
-    head: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["head"][0], _SAMTOOLS_DISPATCH["head"][1])
-
-    sort: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["sort"][0], _SAMTOOLS_DISPATCH["sort"][1])
-
-    mpileup: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["mpileup"][0], _SAMTOOLS_DISPATCH["mpileup"][1])
-
-    consensus: Final[PysamDispatcher] = _wrap_command(
-        _SAMTOOLS_DISPATCH["consensus"][0],
-        _SAMTOOLS_DISPATCH["consensus"][1],
-    )
-
-    depth: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["depth"][0], _SAMTOOLS_DISPATCH["depth"][1])
-
-    faidx: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["faidx"][0], _SAMTOOLS_DISPATCH["faidx"][1])
-
-    fqidx: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["fqidx"][0], _SAMTOOLS_DISPATCH["fqidx"][1])
-
-    tview: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["tview"][0], _SAMTOOLS_DISPATCH["tview"][1])
-
-    index: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["index"][0], _SAMTOOLS_DISPATCH["index"][1])
-
-    idxstats: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["idxstats"][0], _SAMTOOLS_DISPATCH["idxstats"][1])
-
-    fixmate: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["fixmate"][0], _SAMTOOLS_DISPATCH["fixmate"][1])
-
-    flagstat: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["flagstat"][0], _SAMTOOLS_DISPATCH["flagstat"][1])
-
-    calmd: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["calmd"][0], _SAMTOOLS_DISPATCH["calmd"][1])
-
-    merge: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["merge"][0], _SAMTOOLS_DISPATCH["merge"][1])
-
-    markdup: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["markdup"][0], _SAMTOOLS_DISPATCH["markdup"][1])
-
-    rmdup: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["rmdup"][0], _SAMTOOLS_DISPATCH["rmdup"][1])
-
-    reference: Final[PysamDispatcher] = _wrap_command(
-        _SAMTOOLS_DISPATCH["reference"][0],
-        _SAMTOOLS_DISPATCH["reference"][1],
-    )
-
-    reheader: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["reheader"][0], _SAMTOOLS_DISPATCH["reheader"][1])
-
-    reset: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["reset"][0], _SAMTOOLS_DISPATCH["reset"][1])
-
-    cat: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["cat"][0], _SAMTOOLS_DISPATCH["cat"][1])
-
-    targetcut: Final[PysamDispatcher] = _wrap_command(
-        _SAMTOOLS_DISPATCH["targetcut"][0],
-        _SAMTOOLS_DISPATCH["targetcut"][1],
-    )
-
-    phase: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["phase"][0], _SAMTOOLS_DISPATCH["phase"][1])
-
-    bam2fq: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["bam2fq"][0], _SAMTOOLS_DISPATCH["bam2fq"][1])
-
-    dict: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["dict"][0], _SAMTOOLS_DISPATCH["dict"][1])
-
-    addreplacerg: Final[PysamDispatcher] = _wrap_command(
-        _SAMTOOLS_DISPATCH["addreplacerg"][0],
-        _SAMTOOLS_DISPATCH["addreplacerg"][1],
-    )
-
-    pad2unpad: Final[PysamDispatcher] = _wrap_command(
-        _SAMTOOLS_DISPATCH["pad2unpad"][0],
-        _SAMTOOLS_DISPATCH["pad2unpad"][1],
-    )
-
-    depad: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["depad"][0], _SAMTOOLS_DISPATCH["depad"][1])
-
-    bedcov: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["bedcov"][0], _SAMTOOLS_DISPATCH["bedcov"][1])
-
-    coverage: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["coverage"][0], _SAMTOOLS_DISPATCH["coverage"][1])
-
-    bamshuf: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["bamshuf"][0], _SAMTOOLS_DISPATCH["bamshuf"][1])
-
-    collate: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["collate"][0], _SAMTOOLS_DISPATCH["collate"][1])
-
-    stats: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["stats"][0], _SAMTOOLS_DISPATCH["stats"][1])
-
-    fasta: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["fasta"][0], _SAMTOOLS_DISPATCH["fasta"][1])
-
-    fastq: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["fastq"][0], _SAMTOOLS_DISPATCH["fastq"][1])
-
-    cram_size: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["cram_size"][0], _SAMTOOLS_DISPATCH["cram_size"][1])
-
-    quickcheck: Final[PysamDispatcher] = _wrap_command(
-        _SAMTOOLS_DISPATCH["quickcheck"][0],
-        _SAMTOOLS_DISPATCH["quickcheck"][1],
-    )
-
-    split: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["split"][0], _SAMTOOLS_DISPATCH["split"][1])
-
-    flags: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["flags"][0], _SAMTOOLS_DISPATCH["flags"][1])
-
-    ampliconclip: Final[PysamDispatcher] = _wrap_command(
-        _SAMTOOLS_DISPATCH["ampliconclip"][0],
-        _SAMTOOLS_DISPATCH["ampliconclip"][1],
-    )
-
-    ampliconstats: Final[PysamDispatcher] = _wrap_command(
-        _SAMTOOLS_DISPATCH["ampliconstats"][0],
-        _SAMTOOLS_DISPATCH["ampliconstats"][1],
-    )
-
-    version: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["version"][0], _SAMTOOLS_DISPATCH["version"][1])
-
-    fqimport: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["fqimport"][0], _SAMTOOLS_DISPATCH["fqimport"][1])
-
-    import_: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["import_"][0], _SAMTOOLS_DISPATCH["import_"][1])
-
-    samples: Final[PysamDispatcher] = _wrap_command(_SAMTOOLS_DISPATCH["samples"][0], _SAMTOOLS_DISPATCH["samples"][1])
+import pysam.utils
+
+addreplacerg = pysam.utils.PysamDispatcher('samtools', 'addreplacerg')
+ampliconclip = pysam.utils.PysamDispatcher('samtools', 'ampliconclip')
+ampliconstats = pysam.utils.PysamDispatcher('samtools', 'ampliconstats')
+bam2fq = pysam.utils.PysamDispatcher('samtools', 'bam2fq')
+bamshuf = pysam.utils.PysamDispatcher('samtools', 'bamshuf')
+bedcov = pysam.utils.PysamDispatcher('samtools', 'bedcov')
+calmd = pysam.utils.PysamDispatcher('samtools', 'calmd')
+cat = pysam.utils.PysamDispatcher('samtools', 'cat')
+collate = pysam.utils.PysamDispatcher('samtools', 'collate')
+consensus = pysam.utils.PysamDispatcher('samtools', 'consensus')
+coverage = pysam.utils.PysamDispatcher('samtools', 'coverage')
+cram_size = pysam.utils.PysamDispatcher('samtools', 'cram-size')
+depad = pysam.utils.PysamDispatcher('samtools', 'depad')
+depth = pysam.utils.PysamDispatcher('samtools', 'depth')
+dict = pysam.utils.PysamDispatcher('samtools', 'dict')
+faidx = pysam.utils.PysamDispatcher('samtools', 'faidx')
+fasta = pysam.utils.PysamDispatcher('samtools', 'fasta')
+fastq = pysam.utils.PysamDispatcher('samtools', 'fastq')
+fixmate = pysam.utils.PysamDispatcher('samtools', 'fixmate')
+flags = pysam.utils.PysamDispatcher('samtools', 'flags')
+flagstat = pysam.utils.PysamDispatcher('samtools', 'flagstat')
+fqidx = pysam.utils.PysamDispatcher('samtools', 'fqidx')
+fqimport = pysam.utils.PysamDispatcher('samtools', 'import')
+head = pysam.utils.PysamDispatcher('samtools', 'head')
+idxstats = pysam.utils.PysamDispatcher('samtools', 'idxstats')
+index = pysam.utils.PysamDispatcher('samtools', 'index')
+markdup = pysam.utils.PysamDispatcher('samtools', 'markdup')
+merge = pysam.utils.PysamDispatcher('samtools', 'merge')
+mpileup = pysam.utils.PysamDispatcher('samtools', 'mpileup')
+pad2unpad = pysam.utils.PysamDispatcher('samtools', 'pad2unpad')
+phase = pysam.utils.PysamDispatcher('samtools', 'phase')
+quickcheck = pysam.utils.PysamDispatcher('samtools', 'quickcheck')
+reference = pysam.utils.PysamDispatcher('samtools', 'reference')
+reheader = pysam.utils.PysamDispatcher('samtools', 'reheader')
+reset = pysam.utils.PysamDispatcher('samtools', 'reset')
+rmdup = pysam.utils.PysamDispatcher('samtools', 'rmdup')
+samples = pysam.utils.PysamDispatcher('samtools', 'samples')
+sort = pysam.utils.PysamDispatcher('samtools', 'sort')
+split = pysam.utils.PysamDispatcher('samtools', 'split')
+stats = pysam.utils.PysamDispatcher('samtools', 'stats')
+targetcut = pysam.utils.PysamDispatcher('samtools', 'targetcut')
+tview = pysam.utils.PysamDispatcher('samtools', 'tview')
+version = pysam.utils.PysamDispatcher('samtools', 'version')
+view = pysam.utils.PysamDispatcher('samtools', 'view')
+
+__all__ = [
+    'addreplacerg', 'ampliconclip', 'ampliconstats',
+    'bam2fq', 'bamshuf', 'bedcov', 'calmd', 'cat',
+    'collate', 'consensus', 'coverage', 'cram_size',
+    'depad', 'depth', 'dict', 'faidx', 'fasta',
+    'fastq', 'fixmate', 'flags', 'flagstat', 'fqidx',
+    'fqimport', 'head', 'idxstats', 'index',
+    'markdup', 'merge', 'mpileup', 'pad2unpad',
+    'phase', 'quickcheck', 'reference', 'reheader',
+    'reset', 'rmdup', 'samples', 'sort', 'split',
+    'stats', 'targetcut', 'tview', 'version', 'view',
+]
index d15f4317138d09f033c347a9768dd7f7860f45b1..dab58ff61c5aae32b4b43c59155cf655bd9c6794 100644 (file)
@@ -1,6 +1,7 @@
 from typing import (
     Callable,
     List,
+    Optional,
     Tuple,
     Iterable,
     Union,
@@ -48,7 +49,7 @@ class PysamDispatcher(object):
         self,
         collection: str,
         dispatch: str,
-        parsers: Iterable[Tuple[str, Callable[[Union[str, List[str]]], Union[str, List[str]]]]],
+        parsers: Optional[Iterable[Tuple[str, Callable[[Union[str, List[str]]], Union[str, List[str]]]]]] = None,
     ):
         self.collection = collection
         self.dispatch = dispatch
index 645557ba7bb8648365882455b3ca96a153dbaad1..1fb0cff53719206a2834ecbb53bda243ca05f773 100644 (file)
@@ -1,5 +1,5 @@
 // Version information used while compiling samtools, bcftools, and htslib
 
-#define SAMTOOLS_VERSION "1.18 (pysam)"
-#define BCFTOOLS_VERSION "1.18 (pysam)"
-#define HTS_VERSION_TEXT "1.18 (pysam)"
+#define SAMTOOLS_VERSION "1.21 (pysam)"
+#define BCFTOOLS_VERSION "1.21 (pysam)"
+#define HTS_VERSION_TEXT "1.21 (pysam)"
index 8625167b4323ccb34fd96a21918aeaad8e92c967..bdea9ce48d31c5e12f232d83f207b5239aed420c 100644 (file)
@@ -1,6 +1,6 @@
 # pysam versioning information
-__version__ = "0.22.1"
+__version__ = "0.23.0"
 
-__samtools_version__ = "1.18"
-__bcftools_version__ = "1.18"
-__htslib_version__ = "1.18"
+__samtools_version__ = "1.21"
+__bcftools_version__ = "1.21"
+__htslib_version__ = "1.21"
index e72eb636025d537a6f87456b7a3560287c519a61..f096c2de0bf626b54fa4e4a8755efb5040aab2f0 100644 (file)
@@ -1,6 +1,6 @@
 The MIT/Expat License
 
-Copyright (C) 2008-2023 Genome Research Ltd.
+Copyright (C) 2008-2024 Genome Research Ltd.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
index 8f4f2369ab17e6e94badeaa4d0cf6a96d4dadbed..d681d281e2858b76428ffa5a790be76a8083dc58 100644 (file)
@@ -9,7 +9,7 @@ Building samtools
 The typical simple case of building Samtools using the HTSlib bundled within
 this Samtools release tarball is done as follows:
 
-    cd .../samtools-1.18 # Within the unpacked release directory
+    cd .../samtools-1.21 # Within the unpacked release directory
     ./configure
     make
 
@@ -21,7 +21,7 @@ install samtools etc properly into a directory of your choosing.  Building for
 installation using the HTSlib bundled within this Samtools release tarball,
 and building the various HTSlib utilities such as bgzip is done as follows:
 
-    cd .../samtools-1.18 # Within the unpacked release directory
+    cd .../samtools-1.21 # Within the unpacked release directory
     ./configure --prefix=/path/to/location
     make all all-htslib
     make install install-htslib
@@ -48,7 +48,7 @@ There are two advantages to this:
 To build with plug-ins, you need to use the --enable-plugins configure option
 as follows:
 
-    cd .../samtools-1.18 # Within the unpacked release directory
+    cd .../samtools-1.21 # Within the unpacked release directory
     ./configure --enable-plugins --prefix=/path/to/location
     make all all-htslib
     make install install-htslib
@@ -66,8 +66,8 @@ Setting --with-plugin-path is useful if you want to run directly from
 the source distribution instead of installing the package.  In that case
 you can use:
 
-    cd .../samtools-1.18 # Within the unpacked release directory
-    ./configure --enable-plugins --with-plugin-path=$PWD/htslib-1.18
+    cd .../samtools-1.21 # Within the unpacked release directory
+    ./configure --enable-plugins --with-plugin-path=$PWD/htslib-1.21
     make all all-htslib
 
 It is possible to override the built-in search path using the HTS_PATH
index 3842fb319fcba158b5e29596578e791877493835..5dff050c32e9f0461b81f0d7edaa196702e73837 100644 (file)
@@ -1,6 +1,6 @@
 /*  stats.c -- This is the former bamcheck integrated into samtools/htslib.
 
-    Copyright (C) 2020-2021 Genome Research Ltd.
+    Copyright (C) 2020-2021, 2024 Genome Research Ltd.
 
     Author: James Bonfield <jkb@sanger.ac.uk>
 
@@ -1652,15 +1652,34 @@ int main_ampliconstats(int argc, char **argv) {
         {"single-ref", no_argument, NULL, 'S'},
         {NULL, 0, NULL, 0}
     };
-    int opt;
+    int opt, tmp_flag;
 
     while ( (opt=getopt_long(argc,argv,"?hf:F:@:p:m:d:sa:l:t:o:c:b:D:S",loptions,NULL))>0 ) {
         switch (opt) {
-        case 'f': args.flag_require = bam_str2flag(optarg); break;
+        case 'f':
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("ampliconstats", "Unknown flag '%s'\n", optarg);
+                return 1;
+            }
+
+            args.flag_require = tmp_flag;
+            break;
+
         case 'F':
+            tmp_flag = bam_str2flag(optarg);
+
             if (args.flag_filter & 0x10000)
                 args.flag_filter = 0; // strip default on first -F usage
-            args.flag_filter |= bam_str2flag(optarg); break;
+
+            if (tmp_flag < 0) {
+                print_error("ampliconstats", "Unknown flag '%s'\n", optarg);
+                return 1;
+            }
+
+            args.flag_filter |= tmp_flag;
+            break;
 
         case 'm': args.max_delta = atoi(optarg); break; // margin
         case 'D': args.depth_bin = atof(optarg); break; // depth bin fraction
@@ -1718,7 +1737,7 @@ int main_ampliconstats(int argc, char **argv) {
         return usage(&oargs, stderr, EXIT_FAILURE);
 
     khash_t(bed_list_hash) *bed_hash = kh_init(bed_list_hash);
-    if (load_bed_file_multi_ref(argv[optind], 1, 0, bed_hash)) {
+    if (load_bed_file_multi_ref(argv[optind], 1, 0, bed_hash, NULL, NULL)) {
         print_error_errno("ampliconstats",
                           "Could not read file \"%s\"", argv[optind]);
         return 1;
index b71ac4adf1d28c16978debbf3692ab514bf76650..99e804b57637f73d0d9989ca4e61951eaa16bd48 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  stats.c -- This is the former bamcheck integrated into samtools/htslib.
 
-    Copyright (C) 2020-2021 Genome Research Ltd.
+    Copyright (C) 2020-2021, 2024 Genome Research Ltd.
 
     Author: James Bonfield <jkb@sanger.ac.uk>
 
@@ -1654,15 +1654,34 @@ int main_ampliconstats(int argc, char **argv) {
         {"single-ref", no_argument, NULL, 'S'},
         {NULL, 0, NULL, 0}
     };
-    int opt;
+    int opt, tmp_flag;
 
     while ( (opt=getopt_long(argc,argv,"?hf:F:@:p:m:d:sa:l:t:o:c:b:D:S",loptions,NULL))>0 ) {
         switch (opt) {
-        case 'f': args.flag_require = bam_str2flag(optarg); break;
+        case 'f':
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("ampliconstats", "Unknown flag '%s'\n", optarg);
+                return 1;
+            }
+
+            args.flag_require = tmp_flag;
+            break;
+
         case 'F':
+            tmp_flag = bam_str2flag(optarg);
+
             if (args.flag_filter & 0x10000)
                 args.flag_filter = 0; // strip default on first -F usage
-            args.flag_filter |= bam_str2flag(optarg); break;
+
+            if (tmp_flag < 0) {
+                print_error("ampliconstats", "Unknown flag '%s'\n", optarg);
+                return 1;
+            }
+
+            args.flag_filter |= tmp_flag;
+            break;
 
         case 'm': args.max_delta = atoi(optarg); break; // margin
         case 'D': args.depth_bin = atof(optarg); break; // depth bin fraction
@@ -1720,7 +1739,7 @@ int main_ampliconstats(int argc, char **argv) {
         return usage(&oargs, samtools_stderr, EXIT_FAILURE);
 
     khash_t(bed_list_hash) *bed_hash = kh_init(bed_list_hash);
-    if (load_bed_file_multi_ref(argv[optind], 1, 0, bed_hash)) {
+    if (load_bed_file_multi_ref(argv[optind], 1, 0, bed_hash, NULL, NULL)) {
         print_error_errno("ampliconstats",
                           "Could not read file \"%s\"", argv[optind]);
         return 1;
index 9266b6199aae60a179cdba992635a88607606460..c3e4f04d4e62cd5f994e8f1bc7ccd8671b28c470 100644 (file)
@@ -1,7 +1,7 @@
 /*  bam2depth.c -- depth subcommand.
 
     Copyright (C) 2011, 2012 Broad Institute.
-    Copyright (C) 2012-2016, 2018, 2019-2022 Genome Research Ltd.
+    Copyright (C) 2012-2016, 2018, 2019-2022, 2024 Genome Research Ltd.
 
     Author: Heng Li <lh3@sanger.ac.uk> (to 2020)
     Author: James Bonfield <jkb@sanger.ac.uk> (2021 rewrite)
@@ -158,6 +158,42 @@ hts_pos_t qlen_used(bam1_t *b) {
 
 }
 
+// Without HTS_OPT3 gcc11 and earlier don't vectorize this, nor even
+// unroll it.  By adding HTS_OPT3 we can force a better level of optimization.
+// On an Illumina BAM with gcc11 -O2, with HTS_OPT3 is 9% quicker for the
+// entire process (ie decompress, iterator, aggregate & report)
+static inline void HTS_OPT3 incr_hist(int *hist, int oplen) {
+    const int N = 16;
+    int k;
+    for (k = 0; k < (oplen & ~(N-1)); k+=N) {
+        for (int i = 0; i < N; i++)
+            hist[k+i]++;
+    }
+    for (; k < oplen; k++)
+        hist[k]++;
+}
+
+static inline void HTS_OPT3
+incr_hist_qual(int *hist, uint8_t *qual, int min_qual, int oplen) {
+    if (!min_qual) {
+        incr_hist(hist, oplen);
+        return;
+    }
+
+    int k;
+    for (k = 0; k < (oplen & ~31); k+=32) {
+        int pass[32];
+        // Two separate loops helps clang to vectorize
+        for (int i = 0; i < 32; i++)
+            pass[i]=qual[k+i]>=min_qual;
+
+        for (int i = 0; i < 32; i++)
+            hist[k+i]+=pass[i];
+    }
+    for (; k < oplen; k++)
+        hist[k]+=qual[k]>=min_qual;
+}
+
 // Adds the depth for a single read to a depth_hist struct.
 // For just one file, this is easy.  We just have a circular buffer
 // where we increment values for bits that overlap existing data
@@ -379,7 +415,6 @@ static int add_depth(depth_opt *opt, depth_hist *dh, sam_hdr_t *h, bam1_t *b,
                 // We've explicitly asked to include them, and the quality
                 // is wrong anyway (it's the neighbouring base).  We do this
                 // for now for compatibility with the old depth command.
-
                 if (spos < b->core.l_qseq)
                     for (; k < oplen; k++, i++)
                         hist[i & hmask]+=qual[spos]>=min_qual;
@@ -391,80 +426,30 @@ static int add_depth(depth_opt *opt, depth_hist *dh, sam_hdr_t *h, bam1_t *b,
 
         case BAM_CMATCH:
         case BAM_CEQUAL:
-        case BAM_CDIFF:
-            if ((i & hmask) < ((i+oplen) & hmask)) {
-                // Optimisation when not wrapping around
-
-                // Unrolling doesn't help clang, but helps gcc,
-                // especially when not using -O3.
-                int *hist = &dh->hist[file][i & hmask];
-                if (min_qual || overlap_clip) {
-                    k = 0;
-                    if (overlap_clip) {
-                        if (i+oplen < overlap_clip) {
-                            i += oplen;
-                            spos += oplen;
-                            break;
-                        } else if (i < overlap_clip) {
-                            oplen -= overlap_clip - i;
-                            spos += overlap_clip - i;
-                            hist += overlap_clip - i;
-                            i = overlap_clip;
-                        }
-                    }
-
-                    // approx 50% of this func cpu time in this loop
-                    for (; k < (oplen & ~7); k+=8) {
-                        hist[k+0]+=qual[spos+0]>=min_qual;
-                        hist[k+1]+=qual[spos+1]>=min_qual;
-                        hist[k+2]+=qual[spos+2]>=min_qual;
-                        hist[k+3]+=qual[spos+3]>=min_qual;
-                        hist[k+4]+=qual[spos+4]>=min_qual;
-                        hist[k+5]+=qual[spos+5]>=min_qual;
-                        hist[k+6]+=qual[spos+6]>=min_qual;
-                        hist[k+7]+=qual[spos+7]>=min_qual;
-                        spos += 8;
-                    }
-                } else {
-                    // easier to vectorize when no min_qual
-                    for (k = 0; k < (oplen & ~7); k+=8) {
-                        hist[k+0]++;
-                        hist[k+1]++;
-                        hist[k+2]++;
-                        hist[k+3]++;
-                        hist[k+4]++;
-                        hist[k+5]++;
-                        hist[k+6]++;
-                        hist[k+7]++;
-                    }
-                    spos += k;
-                }
-                for (; k < oplen && spos < b->core.l_qseq; k++, spos++)
-                    hist[k]+=qual[spos]>=min_qual;
-                for (; k < oplen; k++, spos++)
-                    hist[k]++;
-                i += oplen;
-            } else {
-                // Simple to understand case, but slower.
-                // We use this only for reads with wrap-around.
-                int *hist = dh->hist[file];
-                k = 0;
-                if (overlap_clip) {
-                    if (i+oplen < overlap_clip) {
-                        i += oplen;
-                        break;
-                    } else if (i < overlap_clip) {
-                        oplen -= overlap_clip - i;
-                        spos += overlap_clip - i;
-                        i = overlap_clip;
-                    }
+        case BAM_CDIFF: {
+            int *hist = dh->hist[file];
+            k = 0;
+            if (overlap_clip) {
+                if (i+oplen < overlap_clip) {
+                    i += oplen;
+                    break;
+                } else if (i < overlap_clip) {
+                    oplen -= overlap_clip - i;
+                    spos += overlap_clip - i;
+                    i = overlap_clip;
                 }
-                for (; k < oplen && spos < b->core.l_qseq; k++, i++, spos++)
-                    hist[i & hmask]+=qual[spos]>=min_qual;
-                for (; k < oplen; k++, i++, spos++)
-                    hist[i & hmask]++;
             }
+
+            int len = (i & hmask) < ((i+oplen) & hmask)
+                ? oplen                  // doesn't wrap around
+                : dh->size - (i&hmask);  // does wrap around
+            incr_hist_qual(&hist[i & hmask], &qual[spos], min_qual, len);
+            if (oplen > len)
+                incr_hist_qual(hist, &qual[spos+len], min_qual, oplen-len);
+            spos += oplen;
+            i += oplen;
             break;
+        }
 
         case BAM_CINS:
         case BAM_CSOFT_CLIP:
@@ -745,7 +730,7 @@ static void usage_exit(FILE *fp, int exit_status)
 
 int main_depth(int argc, char *argv[])
 {
-    int nfiles, i;
+    int nfiles, i, tmp_flag;
     samFile **fp;
     sam_hdr_t **header;
     int c, has_index_file = 0;
@@ -806,16 +791,44 @@ int main_depth(int argc, char *argv[])
             break;
 
         case 'g':
-            opt.flag &= ~bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("depth", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            opt.flag &= ~tmp_flag;
             break;
         case 'G': // reject if any set
-            opt.flag |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("depth", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            opt.flag |= tmp_flag;
             break;
         case 1: // reject unless at least one set (0 means ignore option)
-            opt.incl_flag |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("depth", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            opt.incl_flag |= tmp_flag;
             break;
         case 2: // reject unless all set
-            opt.require_flag |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("depth", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            opt.require_flag |= tmp_flag;
             break;
 
         case 'l':
index abe6141bed87f5bdcf23423c07754d6919ef31df..1a1176c7421e372e4e8a5685f97a03a4794ba347 100644 (file)
@@ -3,7 +3,7 @@
 /*  bam2depth.c -- depth subcommand.
 
     Copyright (C) 2011, 2012 Broad Institute.
-    Copyright (C) 2012-2016, 2018, 2019-2022 Genome Research Ltd.
+    Copyright (C) 2012-2016, 2018, 2019-2022, 2024 Genome Research Ltd.
 
     Author: Heng Li <lh3@sanger.ac.uk> (to 2020)
     Author: James Bonfield <jkb@sanger.ac.uk> (2021 rewrite)
@@ -160,6 +160,42 @@ hts_pos_t qlen_used(bam1_t *b) {
 
 }
 
+// Without HTS_OPT3 gcc11 and earlier don't vectorize this, nor even
+// unroll it.  By adding HTS_OPT3 we can force a better level of optimization.
+// On an Illumina BAM with gcc11 -O2, with HTS_OPT3 is 9% quicker for the
+// entire process (ie decompress, iterator, aggregate & report)
+static inline void HTS_OPT3 incr_hist(int *hist, int oplen) {
+    const int N = 16;
+    int k;
+    for (k = 0; k < (oplen & ~(N-1)); k+=N) {
+        for (int i = 0; i < N; i++)
+            hist[k+i]++;
+    }
+    for (; k < oplen; k++)
+        hist[k]++;
+}
+
+static inline void HTS_OPT3
+incr_hist_qual(int *hist, uint8_t *qual, int min_qual, int oplen) {
+    if (!min_qual) {
+        incr_hist(hist, oplen);
+        return;
+    }
+
+    int k;
+    for (k = 0; k < (oplen & ~31); k+=32) {
+        int pass[32];
+        // Two separate loops helps clang to vectorize
+        for (int i = 0; i < 32; i++)
+            pass[i]=qual[k+i]>=min_qual;
+
+        for (int i = 0; i < 32; i++)
+            hist[k+i]+=pass[i];
+    }
+    for (; k < oplen; k++)
+        hist[k]+=qual[k]>=min_qual;
+}
+
 // Adds the depth for a single read to a depth_hist struct.
 // For just one file, this is easy.  We just have a circular buffer
 // where we increment values for bits that overlap existing data
@@ -381,7 +417,6 @@ static int add_depth(depth_opt *opt, depth_hist *dh, sam_hdr_t *h, bam1_t *b,
                 // We've explicitly asked to include them, and the quality
                 // is wrong anyway (it's the neighbouring base).  We do this
                 // for now for compatibility with the old depth command.
-
                 if (spos < b->core.l_qseq)
                     for (; k < oplen; k++, i++)
                         hist[i & hmask]+=qual[spos]>=min_qual;
@@ -393,80 +428,30 @@ static int add_depth(depth_opt *opt, depth_hist *dh, sam_hdr_t *h, bam1_t *b,
 
         case BAM_CMATCH:
         case BAM_CEQUAL:
-        case BAM_CDIFF:
-            if ((i & hmask) < ((i+oplen) & hmask)) {
-                // Optimisation when not wrapping around
-
-                // Unrolling doesn't help clang, but helps gcc,
-                // especially when not using -O3.
-                int *hist = &dh->hist[file][i & hmask];
-                if (min_qual || overlap_clip) {
-                    k = 0;
-                    if (overlap_clip) {
-                        if (i+oplen < overlap_clip) {
-                            i += oplen;
-                            spos += oplen;
-                            break;
-                        } else if (i < overlap_clip) {
-                            oplen -= overlap_clip - i;
-                            spos += overlap_clip - i;
-                            hist += overlap_clip - i;
-                            i = overlap_clip;
-                        }
-                    }
-
-                    // approx 50% of this func cpu time in this loop
-                    for (; k < (oplen & ~7); k+=8) {
-                        hist[k+0]+=qual[spos+0]>=min_qual;
-                        hist[k+1]+=qual[spos+1]>=min_qual;
-                        hist[k+2]+=qual[spos+2]>=min_qual;
-                        hist[k+3]+=qual[spos+3]>=min_qual;
-                        hist[k+4]+=qual[spos+4]>=min_qual;
-                        hist[k+5]+=qual[spos+5]>=min_qual;
-                        hist[k+6]+=qual[spos+6]>=min_qual;
-                        hist[k+7]+=qual[spos+7]>=min_qual;
-                        spos += 8;
-                    }
-                } else {
-                    // easier to vectorize when no min_qual
-                    for (k = 0; k < (oplen & ~7); k+=8) {
-                        hist[k+0]++;
-                        hist[k+1]++;
-                        hist[k+2]++;
-                        hist[k+3]++;
-                        hist[k+4]++;
-                        hist[k+5]++;
-                        hist[k+6]++;
-                        hist[k+7]++;
-                    }
-                    spos += k;
-                }
-                for (; k < oplen && spos < b->core.l_qseq; k++, spos++)
-                    hist[k]+=qual[spos]>=min_qual;
-                for (; k < oplen; k++, spos++)
-                    hist[k]++;
-                i += oplen;
-            } else {
-                // Simple to understand case, but slower.
-                // We use this only for reads with wrap-around.
-                int *hist = dh->hist[file];
-                k = 0;
-                if (overlap_clip) {
-                    if (i+oplen < overlap_clip) {
-                        i += oplen;
-                        break;
-                    } else if (i < overlap_clip) {
-                        oplen -= overlap_clip - i;
-                        spos += overlap_clip - i;
-                        i = overlap_clip;
-                    }
+        case BAM_CDIFF: {
+            int *hist = dh->hist[file];
+            k = 0;
+            if (overlap_clip) {
+                if (i+oplen < overlap_clip) {
+                    i += oplen;
+                    break;
+                } else if (i < overlap_clip) {
+                    oplen -= overlap_clip - i;
+                    spos += overlap_clip - i;
+                    i = overlap_clip;
                 }
-                for (; k < oplen && spos < b->core.l_qseq; k++, i++, spos++)
-                    hist[i & hmask]+=qual[spos]>=min_qual;
-                for (; k < oplen; k++, i++, spos++)
-                    hist[i & hmask]++;
             }
+
+            int len = (i & hmask) < ((i+oplen) & hmask)
+                ? oplen                  // doesn't wrap around
+                : dh->size - (i&hmask);  // does wrap around
+            incr_hist_qual(&hist[i & hmask], &qual[spos], min_qual, len);
+            if (oplen > len)
+                incr_hist_qual(hist, &qual[spos+len], min_qual, oplen-len);
+            spos += oplen;
+            i += oplen;
             break;
+        }
 
         case BAM_CINS:
         case BAM_CSOFT_CLIP:
@@ -747,7 +732,7 @@ static void usage_exit(FILE *fp, int exit_status)
 
 int main_depth(int argc, char *argv[])
 {
-    int nfiles, i;
+    int nfiles, i, tmp_flag;
     samFile **fp;
     sam_hdr_t **header;
     int c, has_index_file = 0;
@@ -808,16 +793,44 @@ int main_depth(int argc, char *argv[])
             break;
 
         case 'g':
-            opt.flag &= ~bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("depth", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            opt.flag &= ~tmp_flag;
             break;
         case 'G': // reject if any set
-            opt.flag |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("depth", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            opt.flag |= tmp_flag;
             break;
         case 1: // reject unless at least one set (0 means ignore option)
-            opt.incl_flag |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("depth", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            opt.incl_flag |= tmp_flag;
             break;
         case 2: // reject unless all set
-            opt.require_flag |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("depth", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            opt.require_flag |= tmp_flag;
             break;
 
         case 'l':
index 72f39bd5d6888d60d796df9c3b28c4575ff532e4..fe911c81e70d843d53f24f04176b84051e96bfb1 100644 (file)
@@ -1,7 +1,7 @@
 /*  bam_ampliconclip.c -- loads amplicon primers from a BED file and cuts reads
                           from the 5' end.
 
-    Copyright (C) 2020-2023 Genome Research Ltd.
+    Copyright (C) 2020-2024 Genome Research Ltd.
 
     Authors: Andrew Whitwham <aw7@sanger.ac.uk>
              Rob Davies <rmd+git@sanger.ac.uk>
@@ -62,6 +62,7 @@ typedef struct {
     int unmap_len;
     char *arg_list;
     char *stats_file;
+    char *primer_counts_file;
     char *rejects_file;
 } cl_param_t;
 
@@ -72,49 +73,63 @@ static int bed_entry_sort(const void *av, const void *bv) {
     return a->right < b->right ? -1 : (a->right == b->right ? 0 : 1);
 }
 
-
-int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash_t(bed_list_hash) *bed_lists) {
+int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash_t(bed_list_hash) *bed_lists, char ***ref_list, size_t *num_refs_out) {
     hFILE *fp;
     int line_count = 0, ret;
+
+    //variables to store the bed file data for each record
+    char ref[1024] = "";
     int64_t left, right;
+    char name[1024] = "", score[1024] = "";
+    char strand;
+
+    //hash table to store clipping results and bed file data
     kstring_t line = KS_INITIALIZE;
     bed_entry_list_t *list;
     khiter_t bed_itr;
 
+    //ordered ref names list
+    size_t ref_list_sz = 0;
+    size_t num_refs = 0;
+
+    if (ref_list)
+        *ref_list = NULL;
+
+    if (num_refs_out)
+        *num_refs_out = 0;
+
     if ((fp = hopen(infile, "r")) == NULL) {
         print_error_errno("amplicon", "unable to open file %s.", infile);
         return 1;
     }
 
-    char ref[1024];
 
     while (line.l = 0, kgetline(&line, (kgets_func *)hgets, fp) >= 0) {
         line_count++;
         int hret;
-        char strand;
 
         if (line.l == 0 || *line.s == '#') continue;
         if (strncmp(line.s, "track ", 6) == 0) continue;
         if (strncmp(line.s, "browser ", 8) == 0) continue;
 
-        if (get_strand) {
-            if (sscanf(line.s, "%1023s %"SCNd64" %"SCNd64" %*s %*s %c",
-                       ref, &left, &right, &strand) != 4) {
-                fprintf(stderr, "[amplicon] error: bad bed file format in line %d of %s.\n"
-                                "(N.B. ref/chrom name limited to 1023 characters.)\n",
-                                    line_count, infile);
-                ret = 1;
-                goto error;
-            }
-        } else {
-            if (sscanf(line.s, "%1023s %"SCNd64" %"SCNd64,
-                       ref, &left, &right) != 3) {
-                fprintf(stderr, "[amplicon] error: bad bed file format in line %d of %s\n"
-                                "(N.B. ref/chrom name limited to 1023 characters.)\n",
-                                    line_count, infile);
-                ret = 1;
-                goto error;
-            }
+        // A list of the maximal number of columns we may want to parse.
+        // There may be more, but we don't use the ones beyond these.
+        const char *const scanf_str =
+            "%1023s %"SCNd64" %"SCNd64" %1023s %1023s %c";
+
+        // Extract the data from the line into the variables.
+        // Variables corresponding to any missing columns will remain
+        // uninitialised.  We asked for all columns, but cols_parsed will
+        // return how many we found which can be validated against num_columns.
+        int cols_parsed = sscanf(line.s, scanf_str, ref,
+                                 &left, &right, name, score, &strand);
+        if (cols_parsed < (get_strand ? 6 : 3)) {
+            fprintf(stderr, "[amplicon] error: invalid bed file format in line %d of %s.\n"
+                    "Parsed %d columns, but need at least %d\n"
+                    "(N.B. ref/chrom name limited to 1023 characters.)\n",
+                    line_count, infile, cols_parsed, get_strand ? 6 : 3);
+            ret = 1;
+            goto error;
         }
 
         bed_itr = kh_get(bed_list_hash, bed_lists, ref);
@@ -128,6 +143,14 @@ int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash
                 goto error;
             }
 
+            if (ref_list) {
+                if (hts_resize(char **, num_refs + 1, &ref_list_sz, ref_list, 0) < 0) {
+                    fprintf(stderr, "[amplicon] error: unable to allocate memory for ref name list.\n");
+                    ret = 1;
+                    goto error;
+                }
+                (*ref_list)[num_refs++] = ref_name;
+            }
             bed_itr = kh_put(bed_list_hash, bed_lists, ref_name, &hret);
 
             if (hret > 0) {
@@ -147,6 +170,7 @@ int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash
             list = &kh_val(bed_lists, bed_itr);
         }
 
+        // add the bed entry to the list, growing the list if necessary
         if (list->length == list->size) {
            bed_entry_t *tmp;
 
@@ -163,6 +187,26 @@ int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash
 
         list->bp[list->length].left  = left;
         list->bp[list->length].right = right;
+        list->bp[list->length].name  = NULL;
+        list->bp[list->length].score = NULL;
+        if (cols_parsed >= 4) {
+            list->bp[list->length].name = strdup(name);
+            if (list->bp[list->length].name == NULL) {
+                fprintf(stderr, "[amplicon] error: unable to allocate memory for name in line %d of %s: %s.\n", line_count, infile, line.s);
+                ret = 1;
+                goto error;
+            }
+        }
+        if (cols_parsed >= 5) {
+            list->bp[list->length].score = strdup(score);
+            if (list->bp[list->length].score == NULL) {
+                fprintf(stderr, "[amplicon] error: unable to allocate memory for score in line %d of %s: %s\n", line_count, infile, line.s);
+                ret = 1;
+                goto error;
+            }
+        }
+
+        list->bp[list->length].num_reads = 0;
 
         if (get_strand) {
             if (strand == '+') {
@@ -198,6 +242,9 @@ int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash
         ret = 1;
     }
 
+    if (num_refs_out)
+        *num_refs_out = num_refs;
+
 error:
     ks_free(&line);
 
@@ -214,7 +261,12 @@ void destroy_bed_hash(khash_t(bed_list_hash) *hash) {
 
     for (itr = kh_begin(hash); itr != kh_end(hash); ++itr) {
        if (kh_exist(hash, itr)) {
-           free(kh_val(hash, itr).bp);
+           bed_entry_list_t list = kh_val(hash, itr);
+           for (int i = 0; i < list.length; i++) {
+               free(list.bp[i].name);
+               free(list.bp[i].score);
+           }
+           free(list.bp);
            free((char *)kh_key(hash, itr));
            kh_key(hash, itr) = NULL;
         }
@@ -227,7 +279,7 @@ void destroy_bed_hash(khash_t(bed_list_hash) *hash) {
 static int matching_clip_site(bed_entry_list_t *sites, hts_pos_t pos,
                               int is_rev, int use_strand, int64_t longest,
                               cl_param_t *param) {
-    int i, size;  // may need this to be variable
+    int i, size, used_i;
     int tol = param->tol;
     int l = 0, mid = sites->length / 2, r = sites->length;
     int pos_tol = is_rev ? (pos > tol ? pos - tol : 0) : pos;
@@ -242,6 +294,7 @@ static int matching_clip_site(bed_entry_list_t *sites, hts_pos_t pos,
     }
 
     size = 0;
+    used_i = -1;
 
     for (i = l; i < sites->length; i++) {
         hts_pos_t mod_left, mod_right;
@@ -268,15 +321,19 @@ static int matching_clip_site(bed_entry_list_t *sites, hts_pos_t pos,
             if (is_rev) {
                 if (size < pos - sites->bp[i].left) {
                     size = pos - sites->bp[i].left;
+                    used_i = i;
                 }
             } else {
                 if (size < sites->bp[i].right - pos) {
                     size = sites->bp[i].right - pos;
+                    used_i = i;
                 }
             }
         }
     }
-
+    if (used_i >= 0) {
+        sites->bp[used_i].num_reads++;
+    }
     return size;
 }
 
@@ -411,7 +468,7 @@ static int bam_trim_left(bam1_t *rec, bam1_t *rec_out, uint32_t bases,
     }
 
     // Copy remaining QUAL
-    memmove(new_qual, orig_qual, rec->core.l_qseq - qry_removed);
+    memmove(new_qual, orig_qual + qry_removed, rec->core.l_qseq - qry_removed);
 
     // Set new l_qseq
     rec_out->core.l_qseq -= qry_removed;
@@ -642,9 +699,13 @@ static int bam_clip(samFile *in, samFile *out, samFile *reject, char *bedfile,
     kstring_t seq = KS_INITIALIZE;
     bed_entry_list_t *sites;
     FILE *stats_fp = stderr;
+    FILE *bed_count_summary_fp = stderr;
     khash_t(bed_list_hash) *bed_hash = kh_init(bed_list_hash);
+    char **bed_ref_list = NULL;
+    size_t num_bed_refs = 0;
 
-    if (load_bed_file_multi_ref(bedfile, param->use_strand, 1, bed_hash)) {
+    if (load_bed_file_multi_ref(bedfile, param->use_strand, 1, bed_hash,
+                                &bed_ref_list, &num_bed_refs)) {
         fprintf(stderr, "[ampliconclip] error: unable to load bed file.\n");
         goto fail;
     }
@@ -938,11 +999,43 @@ static int bam_clip(samFile *in, samFile *out, samFile *reject, char *bedfile,
 
     if (file_open) {
         fclose(stats_fp);
+        file_open = 0;
+    }
+
+    if (param->primer_counts_file) {
+        if ((bed_count_summary_fp = fopen(param->primer_counts_file, "w")) == NULL) {
+            fprintf(stderr, "[ampliconclip] warning: cannot write count summary to %s.\n", param->primer_counts_file);
+        } else {
+            file_open = 1;
+        }
+
+        //print out the number of reads for each bed entry, bedgraph format
+        fprintf(bed_count_summary_fp, "#CHR\tLEFT\tRIGHT\tNAME\tSCORE\tSTRAND\tNUM_CLIPPED\n");
+        size_t refidx;
+        for (refidx = 0; refidx < num_bed_refs; refidx++) {
+            khiter_t itr = kh_get(bed_list_hash, bed_hash, bed_ref_list[refidx]);
+            if (itr >= kh_end(bed_hash)) {
+                fprintf(stderr, "[ampliconclip] error: %s has gone missing from the hash table\n", bed_ref_list[refidx]);
+                goto fail;
+            }
+            sites = &kh_val(bed_hash, itr);
+            int i;
+            for (i = 0; i < sites->length; i++) {
+                char* strand_out = param->use_strand ? (sites->bp[i].rev ? "-" : "+") : ".";
+                fprintf(bed_count_summary_fp, "%s\t%"PRId64"\t%"PRId64"\t%s\t%s\t%s\t%"PRId64"\n",
+                        kh_key(bed_hash, itr), sites->bp[i].left, sites->bp[i].right, sites->bp[i].name,
+                        sites->bp[i].score, strand_out, sites->bp[i].num_reads);
+            }
+        }
+        if (file_open) {
+            fclose(bed_count_summary_fp);
+        }
     }
 
     ret = 0;
 
 fail:
+    free(bed_ref_list);
     destroy_bed_hash(bed_hash);
     ks_free(&oat);
     ks_free(&seq);
@@ -956,25 +1049,26 @@ fail:
 static void usage(void) {
     fprintf(stderr, "Usage: samtools ampliconclip -b BED file <input.bam> -o <output.bam>\n\n");
     fprintf(stderr, "Option: \n");
-    fprintf(stderr, " -b  FILE            BED file of regions (eg amplicon primers) to be removed.\n");
-    fprintf(stderr, " -o  FILE            output file name (default stdout).\n");
-    fprintf(stderr, " -f  FILE            write stats to file name (default stderr)\n");
-    fprintf(stderr, " -u                  Output uncompressed data\n");
-    fprintf(stderr, " --soft-clip         soft clip amplicon primers from reads (default)\n");
-    fprintf(stderr, " --hard-clip         hard clip amplicon primers from reads.\n");
-    fprintf(stderr, " --both-ends         clip on both 5' and 3' ends.\n");
-    fprintf(stderr, " --strand            use strand data from BED file to match read direction.\n");
-    fprintf(stderr, " --clipped           only output clipped reads.\n");
-    fprintf(stderr, " --fail              mark unclipped, mapped reads as QCFAIL.\n");
-    fprintf(stderr, " --filter-len INT    do not output reads INT size or shorter.\n");
-    fprintf(stderr, " --fail-len   INT    mark as QCFAIL reads INT size or shorter.\n");
-    fprintf(stderr, " --unmap-len  INT    unmap reads INT size or shorter, default 0.\n");
-    fprintf(stderr, " --no-excluded       do not write excluded reads (unmapped or QCFAIL).\n");
-    fprintf(stderr, " --rejects-file FILE file to write filtered reads.\n");
-    fprintf(stderr, " --original          for clipped entries add an OA tag with original data.\n");
-    fprintf(stderr, " --keep-tag          for clipped entries keep the old NM and MD tags.\n");
-    fprintf(stderr, " --tolerance         match region within this number of bases, default 5.\n");
-    fprintf(stderr, " --no-PG             do not add an @PG line.\n");
+    fprintf(stderr, " -b  FILE             BED file of regions (eg amplicon primers) to be removed.\n");
+    fprintf(stderr, " -o  FILE             output file name (default: stdout).\n");
+    fprintf(stderr, " -f  FILE             write stats to file name (default: stderr)\n");
+    fprintf(stderr, " -u                   Output uncompressed data\n");
+    fprintf(stderr, " --soft-clip          soft clip amplicon primers from reads (default)\n");
+    fprintf(stderr, " --hard-clip          hard clip amplicon primers from reads.\n");
+    fprintf(stderr, " --both-ends          clip on both 5' and 3' ends.\n");
+    fprintf(stderr, " --strand             use strand data from BED file to match read direction.\n");
+    fprintf(stderr, " --clipped            only output clipped reads.\n");
+    fprintf(stderr, " --fail               mark unclipped, mapped reads as QCFAIL.\n");
+    fprintf(stderr, " --filter-len INT     do not output reads INT size or shorter.\n");
+    fprintf(stderr, " --fail-len   INT     mark as QCFAIL reads INT size or shorter.\n");
+    fprintf(stderr, " --unmap-len  INT     unmap reads INT size or shorter, default 0.\n");
+    fprintf(stderr, " --no-excluded        do not write excluded reads (unmapped or QCFAIL).\n");
+    fprintf(stderr, " --rejects-file FILE  file to write filtered reads.\n");
+    fprintf(stderr, " --primer-counts FILE file to write read counts per bed entry (bedgraph format).\n");
+    fprintf(stderr, " --original           for clipped entries add an OA tag with original data.\n");
+    fprintf(stderr, " --keep-tag           for clipped entries keep the old NM and MD tags.\n");
+    fprintf(stderr, " --tolerance          match region within this number of bases, default 5.\n");
+    fprintf(stderr, " --no-PG              do not add an @PG line.\n");
     sam_global_opt_help(stderr, "-.O..@-.");
     fprintf(stderr, "\nAbout: Soft clips read alignments where they match BED file defined regions.\n"
                     "Default clipping is only on the 5' end.\n\n");
@@ -1004,10 +1098,11 @@ int amplicon_clip_main(int argc, char **argv) {
         {"fail-len", required_argument, NULL, 1010},
         {"no-excluded", no_argument, NULL, 1011},
         {"rejects-file", required_argument, NULL, 1012},
-        {"original", no_argument, NULL, 1013},
-        {"keep-tag", no_argument, NULL, 1014},
-        {"tolerance", required_argument, NULL, 1015},
-        {"unmap-len", required_argument, NULL, 1016},
+        {"primer-counts", required_argument, NULL, 1013},
+        {"original", no_argument, NULL, 1014},
+        {"keep-tag", no_argument, NULL, 1015},
+        {"tolerance", required_argument, NULL, 1016},
+        {"unmap-len", required_argument, NULL, 1017},
         {NULL, 0, NULL, 0}
     };
 
@@ -1028,10 +1123,11 @@ int amplicon_clip_main(int argc, char **argv) {
             case 1010: param.fail_len = atoi(optarg); break;
             case 1011: param.unmapped = 1; break;
             case 1012: param.rejects_file = optarg; break;
-            case 1013: param.oa_tag = 1; break;
-            case 1014: param.del_tag = 0; break;
-            case 1015: param.tol = atoi(optarg); break;
-            case 1016: param.unmap_len = atoi(optarg); break;
+            case 1013: param.primer_counts_file = optarg; break;
+            case 1014: param.oa_tag = 1; break;
+            case 1015: param.del_tag = 0; break;
+            case 1016: param.tol = atoi(optarg); break;
+            case 1017: param.unmap_len = atoi(optarg); break;
             default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
                       /* else fall-through */
             case '?': usage(); exit(1);
index 0c368508e9c7d5d53d31defef7f196cba5a0bf62..0a86fdb310830d03cb036d82403e291affc17d82 100644 (file)
@@ -3,7 +3,7 @@
 /*  bam_ampliconclip.c -- loads amplicon primers from a BED file and cuts reads
                           from the 5' end.
 
-    Copyright (C) 2020-2023 Genome Research Ltd.
+    Copyright (C) 2020-2024 Genome Research Ltd.
 
     Authors: Andrew Whitwham <aw7@sanger.ac.uk>
              Rob Davies <rmd+git@sanger.ac.uk>
@@ -64,6 +64,7 @@ typedef struct {
     int unmap_len;
     char *arg_list;
     char *stats_file;
+    char *primer_counts_file;
     char *rejects_file;
 } cl_param_t;
 
@@ -74,49 +75,63 @@ static int bed_entry_sort(const void *av, const void *bv) {
     return a->right < b->right ? -1 : (a->right == b->right ? 0 : 1);
 }
 
-
-int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash_t(bed_list_hash) *bed_lists) {
+int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash_t(bed_list_hash) *bed_lists, char ***ref_list, size_t *num_refs_out) {
     hFILE *fp;
     int line_count = 0, ret;
+
+    //variables to store the bed file data for each record
+    char ref[1024] = "";
     int64_t left, right;
+    char name[1024] = "", score[1024] = "";
+    char strand;
+
+    //hash table to store clipping results and bed file data
     kstring_t line = KS_INITIALIZE;
     bed_entry_list_t *list;
     khiter_t bed_itr;
 
+    //ordered ref names list
+    size_t ref_list_sz = 0;
+    size_t num_refs = 0;
+
+    if (ref_list)
+        *ref_list = NULL;
+
+    if (num_refs_out)
+        *num_refs_out = 0;
+
     if ((fp = hopen(infile, "r")) == NULL) {
         print_error_errno("amplicon", "unable to open file %s.", infile);
         return 1;
     }
 
-    char ref[1024];
 
     while (line.l = 0, kgetline(&line, (kgets_func *)hgets, fp) >= 0) {
         line_count++;
         int hret;
-        char strand;
 
         if (line.l == 0 || *line.s == '#') continue;
         if (strncmp(line.s, "track ", 6) == 0) continue;
         if (strncmp(line.s, "browser ", 8) == 0) continue;
 
-        if (get_strand) {
-            if (sscanf(line.s, "%1023s %"SCNd64" %"SCNd64" %*s %*s %c",
-                       ref, &left, &right, &strand) != 4) {
-                fprintf(samtools_stderr, "[amplicon] error: bad bed file format in line %d of %s.\n"
-                                "(N.B. ref/chrom name limited to 1023 characters.)\n",
-                                    line_count, infile);
-                ret = 1;
-                goto error;
-            }
-        } else {
-            if (sscanf(line.s, "%1023s %"SCNd64" %"SCNd64,
-                       ref, &left, &right) != 3) {
-                fprintf(samtools_stderr, "[amplicon] error: bad bed file format in line %d of %s\n"
-                                "(N.B. ref/chrom name limited to 1023 characters.)\n",
-                                    line_count, infile);
-                ret = 1;
-                goto error;
-            }
+        // A list of the maximal number of columns we may want to parse.
+        // There may be more, but we don't use the ones beyond these.
+        const char *const scanf_str =
+            "%1023s %"SCNd64" %"SCNd64" %1023s %1023s %c";
+
+        // Extract the data from the line into the variables.
+        // Variables corresponding to any missing columns will remain
+        // uninitialised.  We asked for all columns, but cols_parsed will
+        // return how many we found which can be validated against num_columns.
+        int cols_parsed = sscanf(line.s, scanf_str, ref,
+                                 &left, &right, name, score, &strand);
+        if (cols_parsed < (get_strand ? 6 : 3)) {
+            fprintf(samtools_stderr, "[amplicon] error: invalid bed file format in line %d of %s.\n"
+                    "Parsed %d columns, but need at least %d\n"
+                    "(N.B. ref/chrom name limited to 1023 characters.)\n",
+                    line_count, infile, cols_parsed, get_strand ? 6 : 3);
+            ret = 1;
+            goto error;
         }
 
         bed_itr = kh_get(bed_list_hash, bed_lists, ref);
@@ -130,6 +145,14 @@ int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash
                 goto error;
             }
 
+            if (ref_list) {
+                if (hts_resize(char **, num_refs + 1, &ref_list_sz, ref_list, 0) < 0) {
+                    fprintf(samtools_stderr, "[amplicon] error: unable to allocate memory for ref name list.\n");
+                    ret = 1;
+                    goto error;
+                }
+                (*ref_list)[num_refs++] = ref_name;
+            }
             bed_itr = kh_put(bed_list_hash, bed_lists, ref_name, &hret);
 
             if (hret > 0) {
@@ -149,6 +172,7 @@ int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash
             list = &kh_val(bed_lists, bed_itr);
         }
 
+        // add the bed entry to the list, growing the list if necessary
         if (list->length == list->size) {
            bed_entry_t *tmp;
 
@@ -165,6 +189,26 @@ int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash
 
         list->bp[list->length].left  = left;
         list->bp[list->length].right = right;
+        list->bp[list->length].name  = NULL;
+        list->bp[list->length].score = NULL;
+        if (cols_parsed >= 4) {
+            list->bp[list->length].name = strdup(name);
+            if (list->bp[list->length].name == NULL) {
+                fprintf(samtools_stderr, "[amplicon] error: unable to allocate memory for name in line %d of %s: %s.\n", line_count, infile, line.s);
+                ret = 1;
+                goto error;
+            }
+        }
+        if (cols_parsed >= 5) {
+            list->bp[list->length].score = strdup(score);
+            if (list->bp[list->length].score == NULL) {
+                fprintf(samtools_stderr, "[amplicon] error: unable to allocate memory for score in line %d of %s: %s\n", line_count, infile, line.s);
+                ret = 1;
+                goto error;
+            }
+        }
+
+        list->bp[list->length].num_reads = 0;
 
         if (get_strand) {
             if (strand == '+') {
@@ -200,6 +244,9 @@ int load_bed_file_multi_ref(char *infile, int get_strand, int sort_by_pos, khash
         ret = 1;
     }
 
+    if (num_refs_out)
+        *num_refs_out = num_refs;
+
 error:
     ks_free(&line);
 
@@ -216,7 +263,12 @@ void destroy_bed_hash(khash_t(bed_list_hash) *hash) {
 
     for (itr = kh_begin(hash); itr != kh_end(hash); ++itr) {
        if (kh_exist(hash, itr)) {
-           free(kh_val(hash, itr).bp);
+           bed_entry_list_t list = kh_val(hash, itr);
+           for (int i = 0; i < list.length; i++) {
+               free(list.bp[i].name);
+               free(list.bp[i].score);
+           }
+           free(list.bp);
            free((char *)kh_key(hash, itr));
            kh_key(hash, itr) = NULL;
         }
@@ -229,7 +281,7 @@ void destroy_bed_hash(khash_t(bed_list_hash) *hash) {
 static int matching_clip_site(bed_entry_list_t *sites, hts_pos_t pos,
                               int is_rev, int use_strand, int64_t longest,
                               cl_param_t *param) {
-    int i, size;  // may need this to be variable
+    int i, size, used_i;
     int tol = param->tol;
     int l = 0, mid = sites->length / 2, r = sites->length;
     int pos_tol = is_rev ? (pos > tol ? pos - tol : 0) : pos;
@@ -244,6 +296,7 @@ static int matching_clip_site(bed_entry_list_t *sites, hts_pos_t pos,
     }
 
     size = 0;
+    used_i = -1;
 
     for (i = l; i < sites->length; i++) {
         hts_pos_t mod_left, mod_right;
@@ -270,15 +323,19 @@ static int matching_clip_site(bed_entry_list_t *sites, hts_pos_t pos,
             if (is_rev) {
                 if (size < pos - sites->bp[i].left) {
                     size = pos - sites->bp[i].left;
+                    used_i = i;
                 }
             } else {
                 if (size < sites->bp[i].right - pos) {
                     size = sites->bp[i].right - pos;
+                    used_i = i;
                 }
             }
         }
     }
-
+    if (used_i >= 0) {
+        sites->bp[used_i].num_reads++;
+    }
     return size;
 }
 
@@ -413,7 +470,7 @@ static int bam_trim_left(bam1_t *rec, bam1_t *rec_out, uint32_t bases,
     }
 
     // Copy remaining QUAL
-    memmove(new_qual, orig_qual, rec->core.l_qseq - qry_removed);
+    memmove(new_qual, orig_qual + qry_removed, rec->core.l_qseq - qry_removed);
 
     // Set new l_qseq
     rec_out->core.l_qseq -= qry_removed;
@@ -644,9 +701,13 @@ static int bam_clip(samFile *in, samFile *out, samFile *reject, char *bedfile,
     kstring_t seq = KS_INITIALIZE;
     bed_entry_list_t *sites;
     FILE *stats_fp = samtools_stderr;
+    FILE *bed_count_summary_fp = samtools_stderr;
     khash_t(bed_list_hash) *bed_hash = kh_init(bed_list_hash);
+    char **bed_ref_list = NULL;
+    size_t num_bed_refs = 0;
 
-    if (load_bed_file_multi_ref(bedfile, param->use_strand, 1, bed_hash)) {
+    if (load_bed_file_multi_ref(bedfile, param->use_strand, 1, bed_hash,
+                                &bed_ref_list, &num_bed_refs)) {
         fprintf(samtools_stderr, "[ampliconclip] error: unable to load bed file.\n");
         goto fail;
     }
@@ -940,11 +1001,43 @@ static int bam_clip(samFile *in, samFile *out, samFile *reject, char *bedfile,
 
     if (file_open) {
         fclose(stats_fp);
+        file_open = 0;
+    }
+
+    if (param->primer_counts_file) {
+        if ((bed_count_summary_fp = fopen(param->primer_counts_file, "w")) == NULL) {
+            fprintf(samtools_stderr, "[ampliconclip] warning: cannot write count summary to %s.\n", param->primer_counts_file);
+        } else {
+            file_open = 1;
+        }
+
+        //print out the number of reads for each bed entry, bedgraph format
+        fprintf(bed_count_summary_fp, "#CHR\tLEFT\tRIGHT\tNAME\tSCORE\tSTRAND\tNUM_CLIPPED\n");
+        size_t refidx;
+        for (refidx = 0; refidx < num_bed_refs; refidx++) {
+            khiter_t itr = kh_get(bed_list_hash, bed_hash, bed_ref_list[refidx]);
+            if (itr >= kh_end(bed_hash)) {
+                fprintf(samtools_stderr, "[ampliconclip] error: %s has gone missing from the hash table\n", bed_ref_list[refidx]);
+                goto fail;
+            }
+            sites = &kh_val(bed_hash, itr);
+            int i;
+            for (i = 0; i < sites->length; i++) {
+                char* strand_out = param->use_strand ? (sites->bp[i].rev ? "-" : "+") : ".";
+                fprintf(bed_count_summary_fp, "%s\t%"PRId64"\t%"PRId64"\t%s\t%s\t%s\t%"PRId64"\n",
+                        kh_key(bed_hash, itr), sites->bp[i].left, sites->bp[i].right, sites->bp[i].name,
+                        sites->bp[i].score, strand_out, sites->bp[i].num_reads);
+            }
+        }
+        if (file_open) {
+            fclose(bed_count_summary_fp);
+        }
     }
 
     ret = 0;
 
 fail:
+    free(bed_ref_list);
     destroy_bed_hash(bed_hash);
     ks_free(&oat);
     ks_free(&seq);
@@ -958,25 +1051,26 @@ fail:
 static void usage(void) {
     fprintf(samtools_stderr, "Usage: samtools ampliconclip -b BED file <input.bam> -o <output.bam>\n\n");
     fprintf(samtools_stderr, "Option: \n");
-    fprintf(samtools_stderr, " -b  FILE            BED file of regions (eg amplicon primers) to be removed.\n");
-    fprintf(samtools_stderr, " -o  FILE            output file name (default samtools_stdout).\n");
-    fprintf(samtools_stderr, " -f  FILE            write stats to file name (default samtools_stderr)\n");
-    fprintf(samtools_stderr, " -u                  Output uncompressed data\n");
-    fprintf(samtools_stderr, " --soft-clip         soft clip amplicon primers from reads (default)\n");
-    fprintf(samtools_stderr, " --hard-clip         hard clip amplicon primers from reads.\n");
-    fprintf(samtools_stderr, " --both-ends         clip on both 5' and 3' ends.\n");
-    fprintf(samtools_stderr, " --strand            use strand data from BED file to match read direction.\n");
-    fprintf(samtools_stderr, " --clipped           only output clipped reads.\n");
-    fprintf(samtools_stderr, " --fail              mark unclipped, mapped reads as QCFAIL.\n");
-    fprintf(samtools_stderr, " --filter-len INT    do not output reads INT size or shorter.\n");
-    fprintf(samtools_stderr, " --fail-len   INT    mark as QCFAIL reads INT size or shorter.\n");
-    fprintf(samtools_stderr, " --unmap-len  INT    unmap reads INT size or shorter, default 0.\n");
-    fprintf(samtools_stderr, " --no-excluded       do not write excluded reads (unmapped or QCFAIL).\n");
-    fprintf(samtools_stderr, " --rejects-file FILE file to write filtered reads.\n");
-    fprintf(samtools_stderr, " --original          for clipped entries add an OA tag with original data.\n");
-    fprintf(samtools_stderr, " --keep-tag          for clipped entries keep the old NM and MD tags.\n");
-    fprintf(samtools_stderr, " --tolerance         match region within this number of bases, default 5.\n");
-    fprintf(samtools_stderr, " --no-PG             do not add an @PG line.\n");
+    fprintf(samtools_stderr, " -b  FILE             BED file of regions (eg amplicon primers) to be removed.\n");
+    fprintf(samtools_stderr, " -o  FILE             output file name (default: samtools_stdout).\n");
+    fprintf(samtools_stderr, " -f  FILE             write stats to file name (default: samtools_stderr)\n");
+    fprintf(samtools_stderr, " -u                   Output uncompressed data\n");
+    fprintf(samtools_stderr, " --soft-clip          soft clip amplicon primers from reads (default)\n");
+    fprintf(samtools_stderr, " --hard-clip          hard clip amplicon primers from reads.\n");
+    fprintf(samtools_stderr, " --both-ends          clip on both 5' and 3' ends.\n");
+    fprintf(samtools_stderr, " --strand             use strand data from BED file to match read direction.\n");
+    fprintf(samtools_stderr, " --clipped            only output clipped reads.\n");
+    fprintf(samtools_stderr, " --fail               mark unclipped, mapped reads as QCFAIL.\n");
+    fprintf(samtools_stderr, " --filter-len INT     do not output reads INT size or shorter.\n");
+    fprintf(samtools_stderr, " --fail-len   INT     mark as QCFAIL reads INT size or shorter.\n");
+    fprintf(samtools_stderr, " --unmap-len  INT     unmap reads INT size or shorter, default 0.\n");
+    fprintf(samtools_stderr, " --no-excluded        do not write excluded reads (unmapped or QCFAIL).\n");
+    fprintf(samtools_stderr, " --rejects-file FILE  file to write filtered reads.\n");
+    fprintf(samtools_stderr, " --primer-counts FILE file to write read counts per bed entry (bedgraph format).\n");
+    fprintf(samtools_stderr, " --original           for clipped entries add an OA tag with original data.\n");
+    fprintf(samtools_stderr, " --keep-tag           for clipped entries keep the old NM and MD tags.\n");
+    fprintf(samtools_stderr, " --tolerance          match region within this number of bases, default 5.\n");
+    fprintf(samtools_stderr, " --no-PG              do not add an @PG line.\n");
     sam_global_opt_help(samtools_stderr, "-.O..@-.");
     fprintf(samtools_stderr, "\nAbout: Soft clips read alignments where they match BED file defined regions.\n"
                     "Default clipping is only on the 5' end.\n\n");
@@ -1006,10 +1100,11 @@ int amplicon_clip_main(int argc, char **argv) {
         {"fail-len", required_argument, NULL, 1010},
         {"no-excluded", no_argument, NULL, 1011},
         {"rejects-file", required_argument, NULL, 1012},
-        {"original", no_argument, NULL, 1013},
-        {"keep-tag", no_argument, NULL, 1014},
-        {"tolerance", required_argument, NULL, 1015},
-        {"unmap-len", required_argument, NULL, 1016},
+        {"primer-counts", required_argument, NULL, 1013},
+        {"original", no_argument, NULL, 1014},
+        {"keep-tag", no_argument, NULL, 1015},
+        {"tolerance", required_argument, NULL, 1016},
+        {"unmap-len", required_argument, NULL, 1017},
         {NULL, 0, NULL, 0}
     };
 
@@ -1030,10 +1125,11 @@ int amplicon_clip_main(int argc, char **argv) {
             case 1010: param.fail_len = atoi(optarg); break;
             case 1011: param.unmapped = 1; break;
             case 1012: param.rejects_file = optarg; break;
-            case 1013: param.oa_tag = 1; break;
-            case 1014: param.del_tag = 0; break;
-            case 1015: param.tol = atoi(optarg); break;
-            case 1016: param.unmap_len = atoi(optarg); break;
+            case 1013: param.primer_counts_file = optarg; break;
+            case 1014: param.oa_tag = 1; break;
+            case 1015: param.del_tag = 0; break;
+            case 1016: param.tol = atoi(optarg); break;
+            case 1017: param.unmap_len = atoi(optarg); break;
             default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
                       /* else fall-through */
             case '?': usage(); samtools_exit(1);
index ef3535702a5e6762e4a05c8d9b68cc3d8cfcb935..d17e74fd3d29908086f22046de846deb5a314ef7 100644 (file)
@@ -30,7 +30,10 @@ DEALINGS IN THE SOFTWARE.  */
 typedef struct {
     int64_t left;
     int64_t right;
+    char *name;
+    char *score;
     int rev;
+    int64_t num_reads;
 } bed_entry_t;
 
 typedef struct {
@@ -40,13 +43,14 @@ typedef struct {
     int size;
 } bed_entry_list_t;
 
-KHASH_MAP_INIT_STR(bed_list_hash, bed_entry_list_t);
+KHASH_MAP_INIT_STR(bed_list_hash, bed_entry_list_t)
 
 #define BED_LIST_INIT {NULL, 0, 0, 0, {0}}
 
 
 int load_bed_file_multi_ref(char *infile, int get_strand,
-                        int sort_by_pos, khash_t(bed_list_hash) *bed_lists);
+                            int sort_by_pos, khash_t(bed_list_hash) *bed_lists,
+                            char ***ref_list, size_t *num_refs);
 
 void destroy_bed_hash(khash_t(bed_list_hash) *hash);
 
index ed8cf58c578e623332016d22c4f191ecd08d5e84..ec045c61ba4f034a46e917e9351502828ca1d2cb 100644 (file)
@@ -1,6 +1,7 @@
 /*  bam_cat.c -- efficiently concatenates bam files.
 
-    Copyright (C) 2008-2009, 2011-2013, 2015-2017, 2019, 2021 Genome Research Ltd.
+    Copyright (C) 2008-2009, 2011-2013, 2015-2017, 2019, 2021,
+                  2023-2024 Genome Research Ltd.
     Modified SAMtools work copyright (C) 2010 Illumina, Inc.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -46,55 +47,80 @@ Illumina.
 #include "htslib/sam.h"
 #include "htslib/cram.h"
 #include "htslib/kstring.h"
+#include "htslib/hfile.h"
 #include "samtools.h"
 #include "sam_opts.h"
 
-/*
- * Check the files are consistent and capable of being concatenated.
- * Also fills out the version numbers and produces a new sam_hdr_t
- * structure with merged RG lines.
- * Note it is only a simple merge.
- *
- * Returns updated header on success;
- *        NULL on failure.
- */
-static sam_hdr_t *cram_cat_check_hdr(int nfn, char * const *fn, const sam_hdr_t *h,
-                                     int *vers_maj_p, int *vers_min_p) {
+/// cat_check_merge_hdr - check compatibility and merge RG hearders merges RGon both CRAM and BAM.
+/** @param firstfile - pointer to the 1sr file opened in caller
+ *  @param nfn - number of files to be processed, including the firstfile
+ *  @param fn - array of file paths to be processed
+ *  @param h - sam header pointer which contains explicitly given header
+ *  @param vers_maj_p - cram major version set and send out for output creation
+ *  @param vers_min_p - cram min version set and send out for output creation
+ *  @param out_h - pointer to sam header pointer, outputs the merged header
+ * returns array of opened samFile pointers on success and NULL on failure
+ * This method has the merged header processing for cram and bam.
+ * RG lines are merged for both cram and bam. For cram, version match for each
+ * file and order match of RG lines are compared as well.
+ * Note: it is a simple merge of RG lines alone.
+*/
+static samFile** cat_check_merge_hdr(samFile * const firstfile, int nfn, char * const *fn, const sam_hdr_t *h,
+                                     int *vers_maj_p, int *vers_min_p, sam_hdr_t **out_h) {
     int i, vers_maj = -1, vers_min = -1;
     sam_hdr_t *new_h = NULL, *old_h = NULL;
     samFile *in = NULL;
     kstring_t ks = KS_INITIALIZE;
-
-    if (h) {
-        new_h = sam_hdr_dup(h);
-        if (!new_h) {
-            fprintf(stderr, "[%s] ERROR: header duplication failed.\n",
-                    __func__);
-            goto fail;
+    samFile **files = calloc(nfn, sizeof(samFile *));
+    if(!files) {
+        fprintf(stderr, "[%s] ERROR: failed to allocate space for file handles.\n", __func__);
+        return NULL;
+    }
+    if (!out_h || !firstfile) {
+        fprintf(stderr, "[%s] ERROR: header check failed.\n", __func__);
+        goto fail;
+    }
+    if (*out_h) {           //use header if one is already present
+        new_h = *out_h;
+    }
+    else {
+        if (h) {            //use the explicit header given
+            new_h = sam_hdr_dup(h);
+            if (!new_h) {
+                fprintf(stderr, "[%s] ERROR: header duplication failed.\n",
+                        __func__);
+                goto fail;
+            }
         }
     }
 
     for (i = 0; i < nfn; ++i) {
-        cram_fd *in_c;
         int ki;
-
-        in = sam_open(fn[i], "rc");
+        //1st file is already open and passed, rest open locally
+        files[i] = in = i ? sam_open(fn[i], "r") : firstfile;
         if (in == 0) {
             print_error_errno("cat", "fail to open file '%s'", fn[i]);
             goto fail;
         }
-        in_c = in->fp.cram;
-
-        int vmaj = cram_major_vers(in_c);
-        int vmin = cram_minor_vers(in_c);
-        if ((vers_maj != -1 && vers_maj != vmaj) ||
-            (vers_min != -1 && vers_min != vmin)) {
-            fprintf(stderr, "[%s] ERROR: input files have differing version numbers.\n",
-                    __func__);
+        if (firstfile->format.format != in->format.format) {
+            print_error("cat", "File %s is of different format!", fn[i]);
             goto fail;
         }
-        vers_maj = vmaj;
-        vers_min = vmin;
+        if (firstfile->format.format == cram) {     //version check for cram
+            cram_fd *in_c;
+            in_c = in->fp.cram;
+
+            int vmaj = cram_major_vers(in_c);
+            int vmin = cram_minor_vers(in_c);
+            if ((vers_maj != -1 && vers_maj != vmaj) ||
+                (vers_min != -1 && vers_min != vmin)) {
+                fprintf(stderr, "[%s] ERROR: input files have differing version numbers.\n",
+                        __func__);
+                goto fail;
+            }
+            vers_maj = vmaj;
+            vers_min = vmin;
+        }
 
         old_h = sam_hdr_read(in);
         if (!old_h) {
@@ -111,10 +137,10 @@ static sam_hdr_t *cram_cat_check_hdr(int nfn, char * const *fn, const sam_hdr_t
                 goto fail;
             }
             sam_hdr_destroy(old_h);
-            sam_close(in);
+            old_h = NULL;
             continue;
         }
-
+        //merge RG lines
         int old_count = sam_hdr_count_lines(old_h, "RG");
         for (ki = 0; ki < old_count; ki++) {
             const char *old_name = sam_hdr_line_name(old_h, "RG", ki);
@@ -136,7 +162,8 @@ static sam_hdr_t *cram_cat_check_hdr(int nfn, char * const *fn, const sam_hdr_t
             }
         }
 
-        if (old_count > 1 && sam_hdr_count_lines(new_h, "RG") == old_count) {
+        if (firstfile->format.format == cram && old_count > 1 && sam_hdr_count_lines(new_h, "RG") == old_count) {
+            //RG order check for cram
             for (ki = 0; ki < old_count; ki++) {
                 const char *old_name = sam_hdr_line_name(old_h, "RG", ki);
                 const char *new_name = sam_hdr_line_name(new_h, "RG", ki);
@@ -148,107 +175,281 @@ static sam_hdr_t *cram_cat_check_hdr(int nfn, char * const *fn, const sam_hdr_t
             }
         }
 
-        sam_hdr_destroy(old_h);
-        sam_close(in);
+        sam_hdr_destroy(old_h); old_h = NULL;
     }
 
     ks_free(&ks);
 
-    *vers_maj_p = vers_maj;
-    *vers_min_p = vers_min;
-
-    return new_h;
+    if (vers_maj_p) {
+        *vers_maj_p = vers_maj;
+    }
+    if (vers_min_p) {
+        *vers_min_p = vers_min;
+    }
+    *out_h = new_h;
+    return files;
 
 fail:
     ks_free(&ks);
     if (old_h) sam_hdr_destroy(old_h);
     if (new_h) sam_hdr_destroy(new_h);
-    if (in) sam_close(in);
+    *out_h = NULL;
+    for (i = 1; i < nfn; ++i) {         //close files other than the firstfile
+        if (files[i]) {
+            sam_close(files[i]);
+        }
+    }
+    free(files);
 
     return NULL;
 }
 
 
-/*
- * CRAM files don't store the RG:Z:ID per read in the aux field.
- * Instead they have a numerical data series (RG) to point each read
- * back to the Nth @RG line in the file.  This means that we may need
- * to edit the RG data series (if the files were produced from
- * "samtools split" for example).
- *
- * The encoding method is stored in the compression header. Typical
- * examples:
- *
- * RG => EXTERNAL {18}           # Block content-id 18 holds RG values
- *                               # as a series of ITF8 encoded values
- *
- * RG => HUFFMAN {1, 255, 255, 255, 255, 255, 1, 0}
- *                               # One RG value #-1.  (No RG)
- *
- * RG => HUFFMAN {1, 0, 1, 0}    # One RG value #0 (always first RG)
- *
- * RG => HUFFMAN {2, 0, 1, 2, 1, 1}
- *                               # Two RG values, #0 and #1, written
- *                               # to the CORE block and possibly
- *                               # mixed with other data series.
- *
- * A single value can (but may not be) implemented as a zero bit
- * huffman code.  In this situation we can change the meta-data in the
- * compression header to renumber an RG value..
+/* ----------------------------------------------------------------------
+ * CRAM cat
  */
-int cram_cat(int nfn, char * const *fn, const sam_hdr_t *h, const char* outcram, sam_global_args *ga, char *arg_list, int no_pg)
+
+// Reports the number of CRAM containers spanning a specified region if
+// specified, or the entire file if not.
+// This is the implements the "samtools cat -q [-r reg]" functionality.
+//
+// Returns 0 on success, <0 on error.
+static int cram_query_ncont(int nfn, char * const *fn, char *reg) {
+    int i;
+    hts_idx_t *idx = NULL;
+    sam_hdr_t *hdr = NULL;
+    hts_itr_t *iter = NULL;
+    samFile *in = NULL;
+
+    for (i = 0; i < nfn; i++) {
+        in = sam_open(fn[i], "r");
+        if (!in) {
+            print_error_errno("cat", "Couldn't open file %s", fn[i]);
+            return -1;
+        }
+        idx = sam_index_load(in, fn[i]);
+        if (!idx) {
+            print_error("cat", "No index found");
+            goto err;
+        }
+
+        off_t cstart = 0, cend = 0;
+        if (reg) {
+            sam_hdr_t *hdr = sam_hdr_read(in);
+            hts_itr_t *iter = sam_itr_querys(idx, hdr, reg);
+            if (!hdr) {
+                print_error("cat", "Unable to read header");
+                goto err;
+            }
+            if (!iter) {
+                print_error("cat", "Unable to parse region");
+                goto err;
+            }
+
+            if (cram_index_extents(in->fp.cram, iter->tid, iter->beg,
+                                   iter->end, &cstart, &cend) < 0) {
+                print_error("cat", "Failed to query index");
+                goto err;
+            }
+
+            hts_itr_destroy(iter);
+            sam_hdr_destroy(hdr);
+        }
+
+        int64_t first, last;
+        int64_t nc = cram_num_containers_between(in->fp.cram, cstart, cend,
+                                                 &first, &last);
+        printf("%s\t%"PRId64"\t%"PRId64"\t%"PRId64"\n",
+               fn[i], nc, first, last);
+        sam_close(in);
+        hts_idx_destroy(idx);
+    }
+
+    return 0;
+
+ err:
+    if (idx)
+        hts_idx_destroy(idx);
+    if (hdr)
+        sam_hdr_destroy(hdr);
+    if (iter)
+        hts_itr_destroy(iter);
+
+    sam_close(in);
+
+    return -1;
+}
+
+// Container range #:A-B or #:A.
+// Returns 0 on success, -1 on failure.
+static int cram_handle_cnum_region(cram_fd *fd, hts_idx_t *idx,
+                                   char *reg, off_t *cstart, off_t *cend) {
+    int cnum_start, cnum_end; // container versions
+    int n = sscanf(reg, "#:%d-%d", &cnum_start, &cnum_end);
+
+    if (n == 0) {
+        print_error("cat", "ERROR: Malformed region: %s", reg);
+        return -1;
+    } else if (n == 1) {
+        cnum_end = cnum_start;
+    }
+
+    int64_t nc = cram_num_containers(fd);
+    if (cnum_end >= nc) {
+        print_error("cat", "Too many containers.  "
+                    "The end range should be < %"PRId64, nc);
+        return -1;
+    }
+
+    // Container number to offset
+    *cstart = cram_container_num2offset(fd, cnum_start);
+    *cend   = cram_container_num2offset(fd, cnum_end);
+    if (*cstart < 0 || *cend < 0)
+        return -1;
+
+    // Seek manually
+    return cram_seek(fd, *cstart, SEEK_SET);
+}
+
+// Normal range chr:start-end.
+// Returns an hts iterator on success, NULL on failure.
+static hts_itr_t *cram_handle_region(cram_fd *fd, hts_idx_t *idx, sam_hdr_t *h,
+                                     char *reg, off_t *cstart, off_t *cend) {
+    hts_itr_t *iter;
+
+    if (!idx) {
+        fprintf(stderr, "[%s] ERROR: No index found.\n", __func__);
+        return NULL;
+    }
+
+    // This does an implicit seek and modifies the cram_fd.
+    if (!(iter = sam_itr_querys(idx, h, reg))) {
+        print_error("cat", "Unable to parse region %s", reg);
+        return NULL;
+    }
+    if (cram_index_extents(fd, iter->tid, iter->beg,
+                           iter->end, cstart, cend) < 0) {
+        print_error("cat", "Failed to query index");
+        return NULL;
+    }
+
+    return iter;
+}
+
+// Handle the -p A/B option to subdivide our region or file into portions.
+// Updates cstart/cend.
+// returns 0 on success (do something),
+//         1 on success (but nothing to do),
+//        -1 on failure.
+static int cram_subdivide_part(cram_fd *fd, hts_idx_t *idx, char *part,
+                               off_t *cstart, off_t *cend) {
+    int a, b;
+    // Part N of M
+    if (sscanf(part, "%d/%d", &a, &b) != 2) {
+        print_error("cat", "malformed region %s. Should be e.g. '1/10'", part);
+        return -1;
+    }
+
+    // Inclusive container numbers for range, 0 to NC-1
+    // Our part N/M is in container percentages as we can't have
+    // partial containers, so convert to that first.
+    int64_t cnum1, cnum2;
+    if (cstart) {
+        cnum1 = cram_container_offset2num(fd, *cstart);
+        cnum2 = cram_container_offset2num(fd, *cend);
+    } else {
+        cnum1 = 0;
+        cnum2 = cram_num_containers(fd)-1;
+    }
+
+    // Subdivide cnum1/cnum2 container numbers to new range cnum_start/end
+    int64_t nc = cnum2 - cnum1 + 1;
+    if (b > nc)
+        b = nc;
+
+    int cnum_start = (a-1)*(double)nc/b;
+    int cnum_end   = a*(double)nc/b - 1;
+    if (cnum_start < 0 || cnum_end >= nc)
+        return 1;
+
+    // Then convert back to file offsets so we can seek and do htell
+    // to detect EOR/EOF.
+    *cstart = cram_container_num2offset(fd, cnum_start + cnum1);
+    *cend   = cram_container_num2offset(fd, cnum_end   + cnum1);
+
+    return 0;
+}
+
+// The main cram_cat interface.
+// Returns 0 on success, < 0 on error.
+int cram_cat(samFile * const firstfile, int nfn, char * const *fn,
+             const sam_hdr_t *h, const char* outcram, sam_global_args *ga,
+             char *arg_list, int no_pg, char *reg, char *part, int fast_reg)
 {
-    samFile *out;
+    samFile *out = NULL;
     cram_fd *out_c;
-    int i, vers_maj, vers_min;
+    int i, vers_maj, vers_min, ret = -1;
     sam_hdr_t *new_h = NULL;
-
-    /* Check consistent versioning and compatible headers */
-    if (!(new_h = cram_cat_check_hdr(nfn, fn, h, &vers_maj, &vers_min)))
+    samFile **files = NULL;
+    hts_idx_t *idx = NULL;
+    hts_itr_t *iter = NULL;
+    sam_hdr_t *old_h = NULL;
+
+    // Check consistent versioning and compatible headers;
+    // merges RG lines, opens all files and returns them that multiple
+    // non-seekable stream inputs can be handled
+    if (!(files = cat_check_merge_hdr(firstfile, nfn, fn, h, &vers_maj,
+                                      &vers_min, &new_h)))
         return -1;
 
+    if (!new_h) {
+        print_error_errno("cat", "failed to make output header");
+        goto closefiles;
+    }
+
     /* Open the file with cram_vers */
     char vers[100];
-    sprintf(vers, "%d.%d", vers_maj, vers_min);
-    out = sam_open_format(outcram, "wc", &ga->out);
+    snprintf(vers, sizeof(vers), "%d.%d", vers_maj, vers_min);
+
+    // Can override level=1 with e.g. "--output-fmt-option level=9"
+    out = sam_open_format(outcram, "wc1", &ga->out);
     if (out == 0) {
         print_error_errno("cat", "fail to open output file '%s'", outcram);
-        return -1;
+        goto closefiles;
     }
     out_c = out->fp.cram;
     cram_set_option(out_c, CRAM_OPT_VERSION, vers);
-    //fprintf(stderr, "Creating cram vers %s\n", vers);
 
     if (!no_pg && sam_hdr_add_pg(new_h, "samtools",
                                  "VN", samtools_version(),
                                  arg_list ? "CL": NULL,
                                  arg_list ? arg_list : NULL,
                                  NULL))
-        return -1;
+        goto closefiles;
 
     if (sam_hdr_write(out, new_h) < 0) {
         print_error_errno("cat", "Couldn't write header");
-        return -1;
+        goto closefiles;
     }
+    out_c = out->fp.cram;
 
     for (i = 0; i < nfn; ++i) {
         samFile *in;
         cram_fd *in_c;
         cram_container *c;
-        sam_hdr_t *old_h;
         int new_rg = -1;
 
-        in = sam_open(fn[i], "rc");
+        in = files[i];
         if (in == 0) {
             print_error_errno("cat", "fail to open file '%s'", fn[i]);
-            return -1;
+            goto closefiles;
         }
         in_c = in->fp.cram;
 
         old_h = sam_hdr_read(in);
         if (!old_h) {
             print_error("cat", "fail to read the header of file '%s'", fn[i]);
-            return -1;
+            goto closefiles;
         }
 
         // Compute RG mapping if suitable for changing.
@@ -258,71 +459,237 @@ int cram_cat(int nfn, char * const *fn, const sam_hdr_t *h, const char* outcram,
                 new_rg = sam_hdr_line_index(new_h, "RG", old_name);
                 if (new_rg < 0) {
                     print_error("cat", "fail to find @RG line '%s' in the new header", old_name);
-                    return -1;
+                    goto closefiles;
                 }
             } else {
                 print_error("cat", "fail to find @RG line in file '%s'", fn[i]);
-                return -1;
+                goto closefiles;
             }
         } else {
             new_rg = 0;
         }
 
+        // We have multiple region syntax.  Either the standard chr:start-end
+        // or a cat-specific #:num-num for explicit container numbers.
+        // Both of these seek to a specific file offset and also have the
+        // end offset known so we can use htell to detect when we're done.
+        // Those offsets are in cstart and cend.
+        //
+        // However for the e.g. -p 1/10 syntax we need to know the container
+        // numbers corresponding to the cstart/cend offsets as we can't
+        // start half way through a container when doing fractions.
+        // These are cnum1 and cnum2.  (We could short cut this for the
+        // #:num-num, but it's simpler to just treat all regions identically.)
+        off_t cstart = 0, cend = 0;
+        int filter_by_cnum = 0;
+
+        if (reg || part) {
+            idx = sam_index_load(in, fn[i]);
+            if (!idx) {
+                print_error("cat", "failed to load index");
+                goto closefiles;
+            }
+        }
+
+        if (reg) {
+            if (strncmp(reg, "#:", 2) == 0) {
+                // Region as container numbers
+                if (cram_handle_cnum_region(in_c, idx, reg, &cstart,&cend) < 0)
+                    goto closefiles;
+
+                filter_by_cnum = 1;
+            } else {
+                // Normal range chr:start-end
+                if (!(iter = cram_handle_region(in_c, idx, old_h, reg,
+                                                &cstart, &cend)))
+                    goto closefiles;
+            }
+        }
+
+        // We can also take a range above and subdivide it into parts.
+        // Eg -r chr1 -p 1/10 (... to -p 10/10).  Part only implies
+        // portions of the entire file.
+        if (part) {
+            int r = cram_subdivide_part(in_c, idx, part, &cstart, &cend);
+            if (r != 0) {
+                if (r > 0) // Not an error, just nothing to do
+                    ret = 0;
+                goto closefiles;
+            }
+        }
+
+        if (cstart) // reg or part
+            if (0 != cram_seek(in_c, cstart, SEEK_SET))
+                goto closefiles;
+
+
+        // Make refid -2 ("*") come after other chromosomes, for easy sort
+        int itid = iter
+            ? (iter->tid == HTS_IDX_NOCOOR ? INT_MAX : iter->tid)
+            : 0;
+        int last_ref_id = -99;
+
+        off_t before_hdr = htell(cram_fd_get_fp(in_c));
+
         // Copy contains and blocks within them
         while ((c = cram_read_container(in_c))) {
             if (cram_container_is_empty(in_c)) {
                 cram_block *blk;
                 // Container compression header
                 if (!(blk = cram_read_block(in_c)))
-                    return -1;
+                    goto closefiles;
                 cram_free_block(blk);
                 cram_free_container(c);
                 continue;
             }
 
+            int filter = 0;
+
             // If we have just one RG key and new_rg != 0 then
             // we need to edit the compression header. IF WE CAN.
             if (new_rg) {
+                if (reg) {
+                    print_error("cat", "Cannot specify a region while "
+                                "transcoding RG lines");
+                    goto closefiles;
+                }
                 int zero = 0;
                 //fprintf(stderr, "Transcode RG %d to %d\n", 0, new_rg);
                 cram_transcode_rg(in_c, out_c, c, 1, &zero, &new_rg);
             } else {
                 int32_t num_slices;
-                cram_block *blk;
 
-                // Not switching rg so do the usual read/write loop
-                if (cram_write_container(out_c, c) != 0)
-                    return -1;
+                if (reg) {
+                    if (before_hdr > cend) {
+                        cram_free_container(c);
+                        break;
+                    }
+                }
 
-                // Container compression header
-                if (!(blk = cram_read_block(in_c)))
-                    return -1;
-                if (cram_write_block(out_c, blk) != 0) {
-                    cram_free_block(blk);
-                    return -1;
+                // For chr:start-end regions, do we need to filter or skip?
+                if (iter && reg) {
+                    int refid;
+                    hts_pos_t start, span, end;
+
+                    cram_container_get_coords(c, &refid, &start, &span);
+                    end = start+span;
+
+                    // Make refid -1 ("*") come after other chromosomes
+                    if (refid == -1)
+                        refid = INT_MAX;
+
+                    if (refid > itid || start > iter->end) {
+                        // Beyond the requested range
+                        break;
+                    } else if (refid == -2) {
+                        // Multi-ref containers.  We only support this if
+                        // the RI data series is in a container by itself.
+                        filter = 3;
+                    } else if (refid < itid || end < iter->beg) {
+                        // Skip, in case of mixed size containers.
+                        // Eg: Use "=", skip "-".
+                        // ==========|======|=
+                        //   ----  ==|==  ==|== ----
+                        //     ----- | ====   ----
+                        filter = 2;
+                    } else if (start < iter->beg || end > iter->end) {
+                        // Container overlaps region.
+                        // Fast mode just copies overlapping containers.
+                        // Slow mode does a precise filtering by reading and
+                        // filtering each record in turn so it's the same as
+                        // a samtools view command.
+                        filter = fast_reg ? 0 : 1;
+                    }
+                    // else we're in an "internal" container, so just copy
                 }
-                cram_free_block(blk);
 
+                if (filter && last_ref_id == -1 && itid == INT_MAX)
+                    // Multi-ref containers consisting solely of ref "*" are
+                    // common, but if it's sorted then we know it's "*" from
+                    // here on so we don't need to filter despite multi-ref.
+                    filter = 0;
 
-                // Container num_blocks can be invalid, due to a bug.
-                // Instead we iterate in slice context instead.
-                (void)cram_container_get_landmarks(c, &num_slices);
-                cram_copy_slice(in_c, out_c, num_slices);
-            }
+                if (filter) {
+                    // Filter or skip
+                    cram_filter_container(in_c, out_c, c, &last_ref_id);
+                } else {
+                    // Copy. Consider adding a cram_copy_container API instead.
 
+                    // Container compression header
+                    cram_block *blk;
+                    if (!(blk = cram_read_block(in_c)))
+                        goto closefiles;
+
+                    // Not switching rg so do the usual read/write loop
+                    if (cram_write_container(out_c, c) != 0)
+                        goto closefiles;
+
+                    // Contatiner compression header
+                    if (cram_write_block(out_c, blk) != 0) {
+                        cram_free_block(blk);
+                        goto closefiles;
+                    }
+
+                    // Container num_blocks can be invalid, due to a bug.
+                    // Instead we iterate in slice context instead.
+                    (void)cram_container_get_landmarks(c, &num_slices);
+                    if (cram_copy_slice(in_c, out_c, num_slices) < 0) {
+                        cram_free_block(blk);
+                        goto closefiles;
+                    }
+                    cram_free_block(blk);
+                }
+            }
             cram_free_container(c);
-        }
 
+            // Location of next container start
+            before_hdr = htell(cram_fd_get_fp(in_c));
+            if (filter_by_cnum && before_hdr > cend)
+                break;
+        }
         sam_hdr_destroy(old_h);
-        sam_close(in);
+        old_h = NULL;
+
+        if (idx) {
+            hts_idx_destroy(idx);
+            idx = NULL;
+        }
+
+        if (iter) {
+            hts_itr_destroy(iter);
+            iter = NULL;
+        }
     }
-    sam_close(out);
-    sam_hdr_destroy(new_h);
+    ret = 0;
 
-    return 0;
-}
+closefiles:
+    if (old_h)
+        sam_hdr_destroy(old_h);
+
+    if (idx)
+        hts_idx_destroy(idx);
+
+    if (iter)
+        hts_itr_destroy(iter);
 
+    if (out)
+        sam_close(out);
+
+    if (new_h)
+        sam_hdr_destroy(new_h);
+
+    for (i = 1; i < nfn; ++i) {     //skip firstfile and close rest
+        if (files[i]) {
+            sam_close(files[i]);
+        }
+    }
+    free(files);
+    return ret;
+}
 
+/* ----------------------------------------------------------------------
+ * BAM cat
+ */
 #define BUF_SIZE 0x10000
 
 #define GZIPID1 31
@@ -330,31 +697,40 @@ int cram_cat(int nfn, char * const *fn, const sam_hdr_t *h, const char* outcram,
 
 #define BGZF_EMPTY_BLOCK_SIZE 28
 
-int bam_cat(int nfn, char * const *fn, sam_hdr_t *h, const char* outbam, char *arg_list, int no_pg)
+int bam_cat(samFile * const firstfile, int nfn, char * const *fn, sam_hdr_t *h, const char* outbam, char *arg_list, int no_pg)
 {
-    BGZF *fp, *in = NULL;
+    BGZF *fp = NULL, *in = NULL;
     uint8_t *buf = NULL;
     uint8_t ebuf[BGZF_EMPTY_BLOCK_SIZE];
     const int es=BGZF_EMPTY_BLOCK_SIZE;
     int i;
+    samFile **files = NULL;
+    sam_hdr_t *new_h = NULL;
 
+    /* merges RG lines, opens all files and returns them that multiple non-seekable
+    stream inputs can be handled */
+    if (!(files = cat_check_merge_hdr(firstfile, nfn, fn, h, NULL, NULL, &new_h)))
+        return -1;
+    if (!new_h) {
+        print_error_errno("cat", "failed to make output header");
+        goto fail;
+    }
     fp = strcmp(outbam, "-")? bgzf_open(outbam, "w") : bgzf_fdopen(fileno(stdout), "w");
     if (fp == 0) {
         print_error_errno("cat", "fail to open output file '%s'", outbam);
-        return -1;
+        goto fail;
     }
-    if (h) {
-        if (!no_pg && sam_hdr_add_pg(h, "samtools",
-                                     "VN", samtools_version(),
-                                     arg_list ? "CL": NULL,
-                                     arg_list ? arg_list : NULL,
-                                     NULL))
-            goto fail;
 
-        if (bam_hdr_write(fp, h) < 0) {
-            print_error_errno("cat", "Couldn't write header");
-            goto fail;
-        }
+    if (!no_pg && sam_hdr_add_pg(new_h, "samtools",
+                                    "VN", samtools_version(),
+                                    arg_list ? "CL": NULL,
+                                    arg_list ? arg_list : NULL,
+                                    NULL))
+        goto fail;
+
+    if (bam_hdr_write(fp, new_h) < 0) {
+        print_error_errno("cat", "Couldn't write header");
+        goto fail;
     }
 
     buf = (uint8_t*) malloc(BUF_SIZE);
@@ -363,35 +739,13 @@ int bam_cat(int nfn, char * const *fn, sam_hdr_t *h, const char* outbam, char *a
         goto fail;
     }
     for(i = 0; i < nfn; ++i){
-        sam_hdr_t *old;
         int len,j;
-
-        in = strcmp(fn[i], "-")? bgzf_open(fn[i], "r") : bgzf_fdopen(fileno(stdin), "r");
+        in = files[i]->fp.bgzf;
         if (in == 0) {
             print_error_errno("cat", "fail to open file '%s'", fn[i]);
             goto fail;
         }
-        if (in->is_write) return -1;
-
-        old = bam_hdr_read(in);
-        if (old == NULL) {
-            fprintf(stderr, "[%s] ERROR: couldn't read header for '%s'.\n",
-                    __func__, fn[i]);
-            goto fail;
-        }
-        if (h == 0 && i == 0) {
-            if (!no_pg && sam_hdr_add_pg(old, "samtools",
-                                         "VN", samtools_version(),
-                                         arg_list ? "CL": NULL,
-                                         arg_list ? arg_list : NULL,
-                                         NULL))
-                goto fail;
-
-            if (bam_hdr_write(fp, old) < 0) {
-                print_error_errno("cat", "Couldn't write header");
-                goto fail;
-            }
-        }
+        if (in->is_write) goto fail;
 
         if (in->block_offset < in->block_length) {
             if (bgzf_write(fp, (char *)in->uncompressed_block + in->block_offset, in->block_length - in->block_offset) < 0) goto write_fail;
@@ -432,39 +786,55 @@ int bam_cat(int nfn, char * const *fn, sam_hdr_t *h, const char* outbam, char *a
                 if (bgzf_raw_write(fp, ebuf, es) < 0) goto write_fail;
             }
         }
-        sam_hdr_destroy(old);
-        bgzf_close(in);
         in = NULL;
     }
     free(buf);
     if (bgzf_close(fp) < 0) {
         fprintf(stderr, "[%s] Error on closing '%s'.\n", __func__, outbam);
-        return -1;
+        goto fail;
+    }
+    for (i = 1; i < nfn; ++i) {     //skip firstfile and close rest
+        if (files[i]) {
+            sam_close(files[i]);
+        }
     }
+    free(files);
+    sam_hdr_destroy(new_h);
     return 0;
 
  write_fail:
     fprintf(stderr, "[%s] Error writing to '%s'.\n", __func__, outbam);
  fail:
-    if (in) bgzf_close(in);
+    if (new_h) {
+        sam_hdr_destroy(new_h);
+    }
     if (fp) bgzf_close(fp);
     free(buf);
+
+    if (files) {
+        for(i = 1; i < nfn; ++i) {  //except the firstfile
+            if(files[i]) {
+                sam_close(files[i]);
+            }
+        }
+        free(files);
+    }
     return -1;
 }
 
-
 int main_cat(int argc, char *argv[])
 {
     sam_hdr_t *h = 0;
     char *outfn = 0;
     char **infns = NULL; // files to concatenate
     int infns_size = 0;
-    int c, ret = 0, no_pg = 0, usage = 0;
+    int c, ret = 0, no_pg = 0, usage = 0, query_ncont = 0, fast_mode = 0;
     samFile *in;
     sam_global_args ga;
+    char *reg = NULL, *part = NULL;
 
     static const struct option lopts[] = {
-        SAM_OPT_GLOBAL_OPTIONS('-', '-', '-', 0, '-', '@'),
+        SAM_OPT_GLOBAL_OPTIONS('-', '-', '-', 0, '-', '-'),
         {"no-PG", no_argument, NULL, 1},
         { NULL, 0, NULL, 0 }
     };
@@ -472,21 +842,22 @@ int main_cat(int argc, char *argv[])
     char *arg_list = NULL;
 
     sam_global_args_init(&ga);
-
-    while ((c = getopt_long(argc, argv, "h:o:b:@:", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "h:o:b:r:p:qf", lopts, NULL)) >= 0) {
         switch (c) {
             case 'h': {
                 samFile *fph = sam_open(optarg, "r");
                 if (fph == 0) {
                     fprintf(stderr, "[%s] ERROR: fail to read the header from '%s'.\n", __func__, optarg);
-                    return 1;
+                    ret = 1;
+                    goto end;
                 }
                 h = sam_hdr_read(fph);
                 if (h == NULL) {
                     fprintf(stderr,
                             "[%s] ERROR: failed to read the header from '%s'.\n",
                             __func__, optarg);
-                    return 1;
+                    ret = 1;
+                    goto end;
                 }
                 sam_close(fph);
                 break;
@@ -512,6 +883,19 @@ int main_cat(int argc, char *argv[])
             case 1:
                 no_pg = 1;
                 break;
+            case 'r':
+                reg = optarg;
+                break;
+            case 'p':
+                part = optarg;
+                break;
+            case 'f':
+                fast_mode = 1;
+                break;
+            case 'q':
+                query_ncont=1;
+                break;
+
             default:
                 if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
                 /* else fall-through */
@@ -521,7 +905,8 @@ int main_cat(int argc, char *argv[])
 
     if (!no_pg && !(arg_list = stringify_argv(argc+1, argv-1))) {
         print_error("cat", "failed to create arg_list");
-        return 1;
+        ret = 1;
+        goto end;
     }
 
     // Append files specified in argv to the list.
@@ -541,34 +926,48 @@ int main_cat(int argc, char *argv[])
         fprintf(stderr, "         -h FILE  copy the header from FILE [default is 1st input file]\n");
         fprintf(stderr, "         -o FILE  output BAM/CRAM\n");
         fprintf(stderr, "         --no-PG  do not add a PG line\n");
-        sam_global_opt_help(stderr, "--..-@-.");
-        return 1;
+        fprintf(stderr, "\nCRAM only options for filtering:\n");
+        fprintf(stderr, "         -r REG   filter to region REG.\n");
+        fprintf(stderr, "                  REG can also be #:cstart-cend for specific container numbers\n");
+        fprintf(stderr, "         -p N/M   Specify part N of M (where N is 1 to M inclusive)\n");
+        fprintf(stderr, "         -f       Fast mode: don't filter containers to exactly match region\n");
+        fprintf(stderr, "         -q       Query the total number of indexed containers\n");
+        fprintf(stderr, "\nStandard options:\n");
+        sam_global_opt_help(stderr, "---.---.");
+        ret = 1;
+        goto end;
     }
 
     in = sam_open(infns[0], "r");
     if (!in) {
         print_error_errno("cat", "failed to open file '%s'", infns[0]);
-        return 1;
+        ret = 1;
+        goto end;
     }
 
     switch (hts_get_format(in)->format) {
     case bam:
-        sam_close(in);
-        if (bam_cat(infns_size+nargv_fns, infns, h, outfn? outfn : "-", arg_list, no_pg) < 0)
+        if (bam_cat(in, infns_size+nargv_fns, infns, h, outfn? outfn : "-", arg_list, no_pg) < 0)
             ret = 1;
         break;
 
     case cram:
-        sam_close(in);
-        if (cram_cat(infns_size+nargv_fns, infns, h, outfn? outfn : "-", &ga, arg_list, no_pg) < 0)
+        if (query_ncont) {
+            if (cram_query_ncont(infns_size+nargv_fns, infns, reg) < 0)
+                ret = 1;
+        } else {
+            if (cram_cat(in, infns_size+nargv_fns, infns, h,
+                         outfn? outfn : "-", &ga, arg_list, no_pg, reg,
+                         part, fast_mode) < 0)
             ret = 1;
+        }
         break;
 
     default:
-        sam_close(in);
         fprintf(stderr, "[%s] ERROR: input is not BAM or CRAM\n", __func__);
-        return 1;
+        ret = 1;
     }
+    sam_close(in);
 
  end:
     if (infns_size > 0) {
@@ -582,6 +981,7 @@ int main_cat(int argc, char *argv[])
     free(arg_list);
     if (h)
         sam_hdr_destroy(h);
+    sam_global_args_free(&ga);
 
     return ret;
 }
index ef2199c78009fab1c3b6155b33ac9d719ba50b44..af70d00e28a393dd66cfdc4b7f4d7d7dc77b6fee 100644 (file)
@@ -2,7 +2,8 @@
 
 /*  bam_cat.c -- efficiently concatenates bam files.
 
-    Copyright (C) 2008-2009, 2011-2013, 2015-2017, 2019, 2021 Genome Research Ltd.
+    Copyright (C) 2008-2009, 2011-2013, 2015-2017, 2019, 2021,
+                  2023-2024 Genome Research Ltd.
     Modified SAMtools work copyright (C) 2010 Illumina, Inc.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -48,55 +49,80 @@ Illumina.
 #include "htslib/sam.h"
 #include "htslib/cram.h"
 #include "htslib/kstring.h"
+#include "htslib/hfile.h"
 #include "samtools.h"
 #include "sam_opts.h"
 
-/*
- * Check the files are consistent and capable of being concatenated.
- * Also fills out the version numbers and produces a new sam_hdr_t
- * structure with merged RG lines.
- * Note it is only a simple merge.
- *
- * Returns updated header on success;
- *        NULL on failure.
- */
-static sam_hdr_t *cram_cat_check_hdr(int nfn, char * const *fn, const sam_hdr_t *h,
-                                     int *vers_maj_p, int *vers_min_p) {
+/// cat_check_merge_hdr - check compatibility and merge RG hearders merges RGon both CRAM and BAM.
+/** @param firstfile - pointer to the 1sr file opened in caller
+ *  @param nfn - number of files to be processed, including the firstfile
+ *  @param fn - array of file paths to be processed
+ *  @param h - sam header pointer which contains explicitly given header
+ *  @param vers_maj_p - cram major version set and send out for output creation
+ *  @param vers_min_p - cram min version set and send out for output creation
+ *  @param out_h - pointer to sam header pointer, outputs the merged header
+ * returns array of opened samFile pointers on success and NULL on failure
+ * This method has the merged header processing for cram and bam.
+ * RG lines are merged for both cram and bam. For cram, version match for each
+ * file and order match of RG lines are compared as well.
+ * Note: it is a simple merge of RG lines alone.
+*/
+static samFile** cat_check_merge_hdr(samFile * const firstfile, int nfn, char * const *fn, const sam_hdr_t *h,
+                                     int *vers_maj_p, int *vers_min_p, sam_hdr_t **out_h) {
     int i, vers_maj = -1, vers_min = -1;
     sam_hdr_t *new_h = NULL, *old_h = NULL;
     samFile *in = NULL;
     kstring_t ks = KS_INITIALIZE;
-
-    if (h) {
-        new_h = sam_hdr_dup(h);
-        if (!new_h) {
-            fprintf(samtools_stderr, "[%s] ERROR: header duplication failed.\n",
-                    __func__);
-            goto fail;
+    samFile **files = calloc(nfn, sizeof(samFile *));
+    if(!files) {
+        fprintf(samtools_stderr, "[%s] ERROR: failed to allocate space for file handles.\n", __func__);
+        return NULL;
+    }
+    if (!out_h || !firstfile) {
+        fprintf(samtools_stderr, "[%s] ERROR: header check failed.\n", __func__);
+        goto fail;
+    }
+    if (*out_h) {           //use header if one is already present
+        new_h = *out_h;
+    }
+    else {
+        if (h) {            //use the explicit header given
+            new_h = sam_hdr_dup(h);
+            if (!new_h) {
+                fprintf(samtools_stderr, "[%s] ERROR: header duplication failed.\n",
+                        __func__);
+                goto fail;
+            }
         }
     }
 
     for (i = 0; i < nfn; ++i) {
-        cram_fd *in_c;
         int ki;
-
-        in = sam_open(fn[i], "rc");
+        //1st file is already open and passed, rest open locally
+        files[i] = in = i ? sam_open(fn[i], "r") : firstfile;
         if (in == 0) {
             print_error_errno("cat", "fail to open file '%s'", fn[i]);
             goto fail;
         }
-        in_c = in->fp.cram;
-
-        int vmaj = cram_major_vers(in_c);
-        int vmin = cram_minor_vers(in_c);
-        if ((vers_maj != -1 && vers_maj != vmaj) ||
-            (vers_min != -1 && vers_min != vmin)) {
-            fprintf(samtools_stderr, "[%s] ERROR: input files have differing version numbers.\n",
-                    __func__);
+        if (firstfile->format.format != in->format.format) {
+            print_error("cat", "File %s is of different format!", fn[i]);
             goto fail;
         }
-        vers_maj = vmaj;
-        vers_min = vmin;
+        if (firstfile->format.format == cram) {     //version check for cram
+            cram_fd *in_c;
+            in_c = in->fp.cram;
+
+            int vmaj = cram_major_vers(in_c);
+            int vmin = cram_minor_vers(in_c);
+            if ((vers_maj != -1 && vers_maj != vmaj) ||
+                (vers_min != -1 && vers_min != vmin)) {
+                fprintf(samtools_stderr, "[%s] ERROR: input files have differing version numbers.\n",
+                        __func__);
+                goto fail;
+            }
+            vers_maj = vmaj;
+            vers_min = vmin;
+        }
 
         old_h = sam_hdr_read(in);
         if (!old_h) {
@@ -113,10 +139,10 @@ static sam_hdr_t *cram_cat_check_hdr(int nfn, char * const *fn, const sam_hdr_t
                 goto fail;
             }
             sam_hdr_destroy(old_h);
-            sam_close(in);
+            old_h = NULL;
             continue;
         }
-
+        //merge RG lines
         int old_count = sam_hdr_count_lines(old_h, "RG");
         for (ki = 0; ki < old_count; ki++) {
             const char *old_name = sam_hdr_line_name(old_h, "RG", ki);
@@ -138,7 +164,8 @@ static sam_hdr_t *cram_cat_check_hdr(int nfn, char * const *fn, const sam_hdr_t
             }
         }
 
-        if (old_count > 1 && sam_hdr_count_lines(new_h, "RG") == old_count) {
+        if (firstfile->format.format == cram && old_count > 1 && sam_hdr_count_lines(new_h, "RG") == old_count) {
+            //RG order check for cram
             for (ki = 0; ki < old_count; ki++) {
                 const char *old_name = sam_hdr_line_name(old_h, "RG", ki);
                 const char *new_name = sam_hdr_line_name(new_h, "RG", ki);
@@ -150,107 +177,281 @@ static sam_hdr_t *cram_cat_check_hdr(int nfn, char * const *fn, const sam_hdr_t
             }
         }
 
-        sam_hdr_destroy(old_h);
-        sam_close(in);
+        sam_hdr_destroy(old_h); old_h = NULL;
     }
 
     ks_free(&ks);
 
-    *vers_maj_p = vers_maj;
-    *vers_min_p = vers_min;
-
-    return new_h;
+    if (vers_maj_p) {
+        *vers_maj_p = vers_maj;
+    }
+    if (vers_min_p) {
+        *vers_min_p = vers_min;
+    }
+    *out_h = new_h;
+    return files;
 
 fail:
     ks_free(&ks);
     if (old_h) sam_hdr_destroy(old_h);
     if (new_h) sam_hdr_destroy(new_h);
-    if (in) sam_close(in);
+    *out_h = NULL;
+    for (i = 1; i < nfn; ++i) {         //close files other than the firstfile
+        if (files[i]) {
+            sam_close(files[i]);
+        }
+    }
+    free(files);
 
     return NULL;
 }
 
 
-/*
- * CRAM files don't store the RG:Z:ID per read in the aux field.
- * Instead they have a numerical data series (RG) to point each read
- * back to the Nth @RG line in the file.  This means that we may need
- * to edit the RG data series (if the files were produced from
- * "samtools split" for example).
- *
- * The encoding method is stored in the compression header. Typical
- * examples:
- *
- * RG => EXTERNAL {18}           # Block content-id 18 holds RG values
- *                               # as a series of ITF8 encoded values
- *
- * RG => HUFFMAN {1, 255, 255, 255, 255, 255, 1, 0}
- *                               # One RG value #-1.  (No RG)
- *
- * RG => HUFFMAN {1, 0, 1, 0}    # One RG value #0 (always first RG)
- *
- * RG => HUFFMAN {2, 0, 1, 2, 1, 1}
- *                               # Two RG values, #0 and #1, written
- *                               # to the CORE block and possibly
- *                               # mixed with other data series.
- *
- * A single value can (but may not be) implemented as a zero bit
- * huffman code.  In this situation we can change the meta-data in the
- * compression header to renumber an RG value..
+/* ----------------------------------------------------------------------
+ * CRAM cat
  */
-int cram_cat(int nfn, char * const *fn, const sam_hdr_t *h, const char* outcram, sam_global_args *ga, char *arg_list, int no_pg)
+
+// Reports the number of CRAM containers spanning a specified region if
+// specified, or the entire file if not.
+// This is the implements the "samtools cat -q [-r reg]" functionality.
+//
+// Returns 0 on success, <0 on error.
+static int cram_query_ncont(int nfn, char * const *fn, char *reg) {
+    int i;
+    hts_idx_t *idx = NULL;
+    sam_hdr_t *hdr = NULL;
+    hts_itr_t *iter = NULL;
+    samFile *in = NULL;
+
+    for (i = 0; i < nfn; i++) {
+        in = sam_open(fn[i], "r");
+        if (!in) {
+            print_error_errno("cat", "Couldn't open file %s", fn[i]);
+            return -1;
+        }
+        idx = sam_index_load(in, fn[i]);
+        if (!idx) {
+            print_error("cat", "No index found");
+            goto err;
+        }
+
+        off_t cstart = 0, cend = 0;
+        if (reg) {
+            sam_hdr_t *hdr = sam_hdr_read(in);
+            hts_itr_t *iter = sam_itr_querys(idx, hdr, reg);
+            if (!hdr) {
+                print_error("cat", "Unable to read header");
+                goto err;
+            }
+            if (!iter) {
+                print_error("cat", "Unable to parse region");
+                goto err;
+            }
+
+            if (cram_index_extents(in->fp.cram, iter->tid, iter->beg,
+                                   iter->end, &cstart, &cend) < 0) {
+                print_error("cat", "Failed to query index");
+                goto err;
+            }
+
+            hts_itr_destroy(iter);
+            sam_hdr_destroy(hdr);
+        }
+
+        int64_t first, last;
+        int64_t nc = cram_num_containers_between(in->fp.cram, cstart, cend,
+                                                 &first, &last);
+        fprintf(samtools_stdout, "%s\t%"PRId64"\t%"PRId64"\t%"PRId64"\n",
+               fn[i], nc, first, last);
+        sam_close(in);
+        hts_idx_destroy(idx);
+    }
+
+    return 0;
+
+ err:
+    if (idx)
+        hts_idx_destroy(idx);
+    if (hdr)
+        sam_hdr_destroy(hdr);
+    if (iter)
+        hts_itr_destroy(iter);
+
+    sam_close(in);
+
+    return -1;
+}
+
+// Container range #:A-B or #:A.
+// Returns 0 on success, -1 on failure.
+static int cram_handle_cnum_region(cram_fd *fd, hts_idx_t *idx,
+                                   char *reg, off_t *cstart, off_t *cend) {
+    int cnum_start, cnum_end; // container versions
+    int n = sscanf(reg, "#:%d-%d", &cnum_start, &cnum_end);
+
+    if (n == 0) {
+        print_error("cat", "ERROR: Malformed region: %s", reg);
+        return -1;
+    } else if (n == 1) {
+        cnum_end = cnum_start;
+    }
+
+    int64_t nc = cram_num_containers(fd);
+    if (cnum_end >= nc) {
+        print_error("cat", "Too many containers.  "
+                    "The end range should be < %"PRId64, nc);
+        return -1;
+    }
+
+    // Container number to offset
+    *cstart = cram_container_num2offset(fd, cnum_start);
+    *cend   = cram_container_num2offset(fd, cnum_end);
+    if (*cstart < 0 || *cend < 0)
+        return -1;
+
+    // Seek manually
+    return cram_seek(fd, *cstart, SEEK_SET);
+}
+
+// Normal range chr:start-end.
+// Returns an hts iterator on success, NULL on failure.
+static hts_itr_t *cram_handle_region(cram_fd *fd, hts_idx_t *idx, sam_hdr_t *h,
+                                     char *reg, off_t *cstart, off_t *cend) {
+    hts_itr_t *iter;
+
+    if (!idx) {
+        fprintf(samtools_stderr, "[%s] ERROR: No index found.\n", __func__);
+        return NULL;
+    }
+
+    // This does an implicit seek and modifies the cram_fd.
+    if (!(iter = sam_itr_querys(idx, h, reg))) {
+        print_error("cat", "Unable to parse region %s", reg);
+        return NULL;
+    }
+    if (cram_index_extents(fd, iter->tid, iter->beg,
+                           iter->end, cstart, cend) < 0) {
+        print_error("cat", "Failed to query index");
+        return NULL;
+    }
+
+    return iter;
+}
+
+// Handle the -p A/B option to subdivide our region or file into portions.
+// Updates cstart/cend.
+// returns 0 on success (do something),
+//         1 on success (but nothing to do),
+//        -1 on failure.
+static int cram_subdivide_part(cram_fd *fd, hts_idx_t *idx, char *part,
+                               off_t *cstart, off_t *cend) {
+    int a, b;
+    // Part N of M
+    if (sscanf(part, "%d/%d", &a, &b) != 2) {
+        print_error("cat", "malformed region %s. Should be e.g. '1/10'", part);
+        return -1;
+    }
+
+    // Inclusive container numbers for range, 0 to NC-1
+    // Our part N/M is in container percentages as we can't have
+    // partial containers, so convert to that first.
+    int64_t cnum1, cnum2;
+    if (cstart) {
+        cnum1 = cram_container_offset2num(fd, *cstart);
+        cnum2 = cram_container_offset2num(fd, *cend);
+    } else {
+        cnum1 = 0;
+        cnum2 = cram_num_containers(fd)-1;
+    }
+
+    // Subdivide cnum1/cnum2 container numbers to new range cnum_start/end
+    int64_t nc = cnum2 - cnum1 + 1;
+    if (b > nc)
+        b = nc;
+
+    int cnum_start = (a-1)*(double)nc/b;
+    int cnum_end   = a*(double)nc/b - 1;
+    if (cnum_start < 0 || cnum_end >= nc)
+        return 1;
+
+    // Then convert back to file offsets so we can seek and do htell
+    // to detect EOR/EOF.
+    *cstart = cram_container_num2offset(fd, cnum_start + cnum1);
+    *cend   = cram_container_num2offset(fd, cnum_end   + cnum1);
+
+    return 0;
+}
+
+// The main cram_cat interface.
+// Returns 0 on success, < 0 on error.
+int cram_cat(samFile * const firstfile, int nfn, char * const *fn,
+             const sam_hdr_t *h, const char* outcram, sam_global_args *ga,
+             char *arg_list, int no_pg, char *reg, char *part, int fast_reg)
 {
-    samFile *out;
+    samFile *out = NULL;
     cram_fd *out_c;
-    int i, vers_maj, vers_min;
+    int i, vers_maj, vers_min, ret = -1;
     sam_hdr_t *new_h = NULL;
-
-    /* Check consistent versioning and compatible headers */
-    if (!(new_h = cram_cat_check_hdr(nfn, fn, h, &vers_maj, &vers_min)))
+    samFile **files = NULL;
+    hts_idx_t *idx = NULL;
+    hts_itr_t *iter = NULL;
+    sam_hdr_t *old_h = NULL;
+
+    // Check consistent versioning and compatible headers;
+    // merges RG lines, opens all files and returns them that multiple
+    // non-seekable stream inputs can be handled
+    if (!(files = cat_check_merge_hdr(firstfile, nfn, fn, h, &vers_maj,
+                                      &vers_min, &new_h)))
         return -1;
 
+    if (!new_h) {
+        print_error_errno("cat", "failed to make output header");
+        goto closefiles;
+    }
+
     /* Open the file with cram_vers */
     char vers[100];
-    sprintf(vers, "%d.%d", vers_maj, vers_min);
-    out = sam_open_format(outcram, "wc", &ga->out);
+    snprintf(vers, sizeof(vers), "%d.%d", vers_maj, vers_min);
+
+    // Can override level=1 with e.g. "--output-fmt-option level=9"
+    out = sam_open_format(outcram, "wc1", &ga->out);
     if (out == 0) {
         print_error_errno("cat", "fail to open output file '%s'", outcram);
-        return -1;
+        goto closefiles;
     }
     out_c = out->fp.cram;
     cram_set_option(out_c, CRAM_OPT_VERSION, vers);
-    //fprintf(samtools_stderr, "Creating cram vers %s\n", vers);
 
     if (!no_pg && sam_hdr_add_pg(new_h, "samtools",
                                  "VN", samtools_version(),
                                  arg_list ? "CL": NULL,
                                  arg_list ? arg_list : NULL,
                                  NULL))
-        return -1;
+        goto closefiles;
 
     if (sam_hdr_write(out, new_h) < 0) {
         print_error_errno("cat", "Couldn't write header");
-        return -1;
+        goto closefiles;
     }
+    out_c = out->fp.cram;
 
     for (i = 0; i < nfn; ++i) {
         samFile *in;
         cram_fd *in_c;
         cram_container *c;
-        sam_hdr_t *old_h;
         int new_rg = -1;
 
-        in = sam_open(fn[i], "rc");
+        in = files[i];
         if (in == 0) {
             print_error_errno("cat", "fail to open file '%s'", fn[i]);
-            return -1;
+            goto closefiles;
         }
         in_c = in->fp.cram;
 
         old_h = sam_hdr_read(in);
         if (!old_h) {
             print_error("cat", "fail to read the header of file '%s'", fn[i]);
-            return -1;
+            goto closefiles;
         }
 
         // Compute RG mapping if suitable for changing.
@@ -260,71 +461,237 @@ int cram_cat(int nfn, char * const *fn, const sam_hdr_t *h, const char* outcram,
                 new_rg = sam_hdr_line_index(new_h, "RG", old_name);
                 if (new_rg < 0) {
                     print_error("cat", "fail to find @RG line '%s' in the new header", old_name);
-                    return -1;
+                    goto closefiles;
                 }
             } else {
                 print_error("cat", "fail to find @RG line in file '%s'", fn[i]);
-                return -1;
+                goto closefiles;
             }
         } else {
             new_rg = 0;
         }
 
+        // We have multiple region syntax.  Either the standard chr:start-end
+        // or a cat-specific #:num-num for explicit container numbers.
+        // Both of these seek to a specific file offset and also have the
+        // end offset known so we can use htell to detect when we're done.
+        // Those offsets are in cstart and cend.
+        //
+        // However for the e.g. -p 1/10 syntax we need to know the container
+        // numbers corresponding to the cstart/cend offsets as we can't
+        // start half way through a container when doing fractions.
+        // These are cnum1 and cnum2.  (We could short cut this for the
+        // #:num-num, but it's simpler to just treat all regions identically.)
+        off_t cstart = 0, cend = 0;
+        int filter_by_cnum = 0;
+
+        if (reg || part) {
+            idx = sam_index_load(in, fn[i]);
+            if (!idx) {
+                print_error("cat", "failed to load index");
+                goto closefiles;
+            }
+        }
+
+        if (reg) {
+            if (strncmp(reg, "#:", 2) == 0) {
+                // Region as container numbers
+                if (cram_handle_cnum_region(in_c, idx, reg, &cstart,&cend) < 0)
+                    goto closefiles;
+
+                filter_by_cnum = 1;
+            } else {
+                // Normal range chr:start-end
+                if (!(iter = cram_handle_region(in_c, idx, old_h, reg,
+                                                &cstart, &cend)))
+                    goto closefiles;
+            }
+        }
+
+        // We can also take a range above and subdivide it into parts.
+        // Eg -r chr1 -p 1/10 (... to -p 10/10).  Part only implies
+        // portions of the entire file.
+        if (part) {
+            int r = cram_subdivide_part(in_c, idx, part, &cstart, &cend);
+            if (r != 0) {
+                if (r > 0) // Not an error, just nothing to do
+                    ret = 0;
+                goto closefiles;
+            }
+        }
+
+        if (cstart) // reg or part
+            if (0 != cram_seek(in_c, cstart, SEEK_SET))
+                goto closefiles;
+
+
+        // Make refid -2 ("*") come after other chromosomes, for easy sort
+        int itid = iter
+            ? (iter->tid == HTS_IDX_NOCOOR ? INT_MAX : iter->tid)
+            : 0;
+        int last_ref_id = -99;
+
+        off_t before_hdr = htell(cram_fd_get_fp(in_c));
+
         // Copy contains and blocks within them
         while ((c = cram_read_container(in_c))) {
             if (cram_container_is_empty(in_c)) {
                 cram_block *blk;
                 // Container compression header
                 if (!(blk = cram_read_block(in_c)))
-                    return -1;
+                    goto closefiles;
                 cram_free_block(blk);
                 cram_free_container(c);
                 continue;
             }
 
+            int filter = 0;
+
             // If we have just one RG key and new_rg != 0 then
             // we need to edit the compression header. IF WE CAN.
             if (new_rg) {
+                if (reg) {
+                    print_error("cat", "Cannot specify a region while "
+                                "transcoding RG lines");
+                    goto closefiles;
+                }
                 int zero = 0;
                 //fprintf(samtools_stderr, "Transcode RG %d to %d\n", 0, new_rg);
                 cram_transcode_rg(in_c, out_c, c, 1, &zero, &new_rg);
             } else {
                 int32_t num_slices;
-                cram_block *blk;
 
-                // Not switching rg so do the usual read/write loop
-                if (cram_write_container(out_c, c) != 0)
-                    return -1;
+                if (reg) {
+                    if (before_hdr > cend) {
+                        cram_free_container(c);
+                        break;
+                    }
+                }
 
-                // Container compression header
-                if (!(blk = cram_read_block(in_c)))
-                    return -1;
-                if (cram_write_block(out_c, blk) != 0) {
-                    cram_free_block(blk);
-                    return -1;
+                // For chr:start-end regions, do we need to filter or skip?
+                if (iter && reg) {
+                    int refid;
+                    hts_pos_t start, span, end;
+
+                    cram_container_get_coords(c, &refid, &start, &span);
+                    end = start+span;
+
+                    // Make refid -1 ("*") come after other chromosomes
+                    if (refid == -1)
+                        refid = INT_MAX;
+
+                    if (refid > itid || start > iter->end) {
+                        // Beyond the requested range
+                        break;
+                    } else if (refid == -2) {
+                        // Multi-ref containers.  We only support this if
+                        // the RI data series is in a container by itself.
+                        filter = 3;
+                    } else if (refid < itid || end < iter->beg) {
+                        // Skip, in case of mixed size containers.
+                        // Eg: Use "=", skip "-".
+                        // ==========|======|=
+                        //   ----  ==|==  ==|== ----
+                        //     ----- | ====   ----
+                        filter = 2;
+                    } else if (start < iter->beg || end > iter->end) {
+                        // Container overlaps region.
+                        // Fast mode just copies overlapping containers.
+                        // Slow mode does a precise filtering by reading and
+                        // filtering each record in turn so it's the same as
+                        // a samtools view command.
+                        filter = fast_reg ? 0 : 1;
+                    }
+                    // else we're in an "internal" container, so just copy
                 }
-                cram_free_block(blk);
 
+                if (filter && last_ref_id == -1 && itid == INT_MAX)
+                    // Multi-ref containers consisting solely of ref "*" are
+                    // common, but if it's sorted then we know it's "*" from
+                    // here on so we don't need to filter despite multi-ref.
+                    filter = 0;
 
-                // Container num_blocks can be invalid, due to a bug.
-                // Instead we iterate in slice context instead.
-                (void)cram_container_get_landmarks(c, &num_slices);
-                cram_copy_slice(in_c, out_c, num_slices);
-            }
+                if (filter) {
+                    // Filter or skip
+                    cram_filter_container(in_c, out_c, c, &last_ref_id);
+                } else {
+                    // Copy. Consider adding a cram_copy_container API instead.
 
+                    // Container compression header
+                    cram_block *blk;
+                    if (!(blk = cram_read_block(in_c)))
+                        goto closefiles;
+
+                    // Not switching rg so do the usual read/write loop
+                    if (cram_write_container(out_c, c) != 0)
+                        goto closefiles;
+
+                    // Contatiner compression header
+                    if (cram_write_block(out_c, blk) != 0) {
+                        cram_free_block(blk);
+                        goto closefiles;
+                    }
+
+                    // Container num_blocks can be invalid, due to a bug.
+                    // Instead we iterate in slice context instead.
+                    (void)cram_container_get_landmarks(c, &num_slices);
+                    if (cram_copy_slice(in_c, out_c, num_slices) < 0) {
+                        cram_free_block(blk);
+                        goto closefiles;
+                    }
+                    cram_free_block(blk);
+                }
+            }
             cram_free_container(c);
-        }
 
+            // Location of next container start
+            before_hdr = htell(cram_fd_get_fp(in_c));
+            if (filter_by_cnum && before_hdr > cend)
+                break;
+        }
         sam_hdr_destroy(old_h);
-        sam_close(in);
+        old_h = NULL;
+
+        if (idx) {
+            hts_idx_destroy(idx);
+            idx = NULL;
+        }
+
+        if (iter) {
+            hts_itr_destroy(iter);
+            iter = NULL;
+        }
     }
-    sam_close(out);
-    sam_hdr_destroy(new_h);
+    ret = 0;
 
-    return 0;
-}
+closefiles:
+    if (old_h)
+        sam_hdr_destroy(old_h);
+
+    if (idx)
+        hts_idx_destroy(idx);
+
+    if (iter)
+        hts_itr_destroy(iter);
 
+    if (out)
+        sam_close(out);
+
+    if (new_h)
+        sam_hdr_destroy(new_h);
+
+    for (i = 1; i < nfn; ++i) {     //skip firstfile and close rest
+        if (files[i]) {
+            sam_close(files[i]);
+        }
+    }
+    free(files);
+    return ret;
+}
 
+/* ----------------------------------------------------------------------
+ * BAM cat
+ */
 #define BUF_SIZE 0x10000
 
 #define GZIPID1 31
@@ -332,31 +699,40 @@ int cram_cat(int nfn, char * const *fn, const sam_hdr_t *h, const char* outcram,
 
 #define BGZF_EMPTY_BLOCK_SIZE 28
 
-int bam_cat(int nfn, char * const *fn, sam_hdr_t *h, const char* outbam, char *arg_list, int no_pg)
+int bam_cat(samFile * const firstfile, int nfn, char * const *fn, sam_hdr_t *h, const char* outbam, char *arg_list, int no_pg)
 {
-    BGZF *fp, *in = NULL;
+    BGZF *fp = NULL, *in = NULL;
     uint8_t *buf = NULL;
     uint8_t ebuf[BGZF_EMPTY_BLOCK_SIZE];
     const int es=BGZF_EMPTY_BLOCK_SIZE;
     int i;
+    samFile **files = NULL;
+    sam_hdr_t *new_h = NULL;
 
+    /* merges RG lines, opens all files and returns them that multiple non-seekable
+    stream inputs can be handled */
+    if (!(files = cat_check_merge_hdr(firstfile, nfn, fn, h, NULL, NULL, &new_h)))
+        return -1;
+    if (!new_h) {
+        print_error_errno("cat", "failed to make output header");
+        goto fail;
+    }
     fp = strcmp(outbam, "-")? bgzf_open(outbam, "w") : bgzf_fdopen(fileno(samtools_stdout), "w");
     if (fp == 0) {
         print_error_errno("cat", "fail to open output file '%s'", outbam);
-        return -1;
+        goto fail;
     }
-    if (h) {
-        if (!no_pg && sam_hdr_add_pg(h, "samtools",
-                                     "VN", samtools_version(),
-                                     arg_list ? "CL": NULL,
-                                     arg_list ? arg_list : NULL,
-                                     NULL))
-            goto fail;
 
-        if (bam_hdr_write(fp, h) < 0) {
-            print_error_errno("cat", "Couldn't write header");
-            goto fail;
-        }
+    if (!no_pg && sam_hdr_add_pg(new_h, "samtools",
+                                    "VN", samtools_version(),
+                                    arg_list ? "CL": NULL,
+                                    arg_list ? arg_list : NULL,
+                                    NULL))
+        goto fail;
+
+    if (bam_hdr_write(fp, new_h) < 0) {
+        print_error_errno("cat", "Couldn't write header");
+        goto fail;
     }
 
     buf = (uint8_t*) malloc(BUF_SIZE);
@@ -365,35 +741,13 @@ int bam_cat(int nfn, char * const *fn, sam_hdr_t *h, const char* outbam, char *a
         goto fail;
     }
     for(i = 0; i < nfn; ++i){
-        sam_hdr_t *old;
         int len,j;
-
-        in = strcmp(fn[i], "-")? bgzf_open(fn[i], "r") : bgzf_fdopen(fileno(stdin), "r");
+        in = files[i]->fp.bgzf;
         if (in == 0) {
             print_error_errno("cat", "fail to open file '%s'", fn[i]);
             goto fail;
         }
-        if (in->is_write) return -1;
-
-        old = bam_hdr_read(in);
-        if (old == NULL) {
-            fprintf(samtools_stderr, "[%s] ERROR: couldn't read header for '%s'.\n",
-                    __func__, fn[i]);
-            goto fail;
-        }
-        if (h == 0 && i == 0) {
-            if (!no_pg && sam_hdr_add_pg(old, "samtools",
-                                         "VN", samtools_version(),
-                                         arg_list ? "CL": NULL,
-                                         arg_list ? arg_list : NULL,
-                                         NULL))
-                goto fail;
-
-            if (bam_hdr_write(fp, old) < 0) {
-                print_error_errno("cat", "Couldn't write header");
-                goto fail;
-            }
-        }
+        if (in->is_write) goto fail;
 
         if (in->block_offset < in->block_length) {
             if (bgzf_write(fp, (char *)in->uncompressed_block + in->block_offset, in->block_length - in->block_offset) < 0) goto write_fail;
@@ -434,39 +788,55 @@ int bam_cat(int nfn, char * const *fn, sam_hdr_t *h, const char* outbam, char *a
                 if (bgzf_raw_write(fp, ebuf, es) < 0) goto write_fail;
             }
         }
-        sam_hdr_destroy(old);
-        bgzf_close(in);
         in = NULL;
     }
     free(buf);
     if (bgzf_close(fp) < 0) {
         fprintf(samtools_stderr, "[%s] Error on closing '%s'.\n", __func__, outbam);
-        return -1;
+        goto fail;
+    }
+    for (i = 1; i < nfn; ++i) {     //skip firstfile and close rest
+        if (files[i]) {
+            sam_close(files[i]);
+        }
     }
+    free(files);
+    sam_hdr_destroy(new_h);
     return 0;
 
  write_fail:
     fprintf(samtools_stderr, "[%s] Error writing to '%s'.\n", __func__, outbam);
  fail:
-    if (in) bgzf_close(in);
+    if (new_h) {
+        sam_hdr_destroy(new_h);
+    }
     if (fp) bgzf_close(fp);
     free(buf);
+
+    if (files) {
+        for(i = 1; i < nfn; ++i) {  //except the firstfile
+            if(files[i]) {
+                sam_close(files[i]);
+            }
+        }
+        free(files);
+    }
     return -1;
 }
 
-
 int main_cat(int argc, char *argv[])
 {
     sam_hdr_t *h = 0;
     char *outfn = 0;
     char **infns = NULL; // files to concatenate
     int infns_size = 0;
-    int c, ret = 0, no_pg = 0, usage = 0;
+    int c, ret = 0, no_pg = 0, usage = 0, query_ncont = 0, fast_mode = 0;
     samFile *in;
     sam_global_args ga;
+    char *reg = NULL, *part = NULL;
 
     static const struct option lopts[] = {
-        SAM_OPT_GLOBAL_OPTIONS('-', '-', '-', 0, '-', '@'),
+        SAM_OPT_GLOBAL_OPTIONS('-', '-', '-', 0, '-', '-'),
         {"no-PG", no_argument, NULL, 1},
         { NULL, 0, NULL, 0 }
     };
@@ -474,21 +844,22 @@ int main_cat(int argc, char *argv[])
     char *arg_list = NULL;
 
     sam_global_args_init(&ga);
-
-    while ((c = getopt_long(argc, argv, "h:o:b:@:", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "h:o:b:r:p:qf", lopts, NULL)) >= 0) {
         switch (c) {
             case 'h': {
                 samFile *fph = sam_open(optarg, "r");
                 if (fph == 0) {
                     fprintf(samtools_stderr, "[%s] ERROR: fail to read the header from '%s'.\n", __func__, optarg);
-                    return 1;
+                    ret = 1;
+                    goto end;
                 }
                 h = sam_hdr_read(fph);
                 if (h == NULL) {
                     fprintf(samtools_stderr,
                             "[%s] ERROR: failed to read the header from '%s'.\n",
                             __func__, optarg);
-                    return 1;
+                    ret = 1;
+                    goto end;
                 }
                 sam_close(fph);
                 break;
@@ -514,6 +885,19 @@ int main_cat(int argc, char *argv[])
             case 1:
                 no_pg = 1;
                 break;
+            case 'r':
+                reg = optarg;
+                break;
+            case 'p':
+                part = optarg;
+                break;
+            case 'f':
+                fast_mode = 1;
+                break;
+            case 'q':
+                query_ncont=1;
+                break;
+
             default:
                 if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
                 /* else fall-through */
@@ -523,7 +907,8 @@ int main_cat(int argc, char *argv[])
 
     if (!no_pg && !(arg_list = stringify_argv(argc+1, argv-1))) {
         print_error("cat", "failed to create arg_list");
-        return 1;
+        ret = 1;
+        goto end;
     }
 
     // Append files specified in argv to the list.
@@ -543,34 +928,48 @@ int main_cat(int argc, char *argv[])
         fprintf(samtools_stderr, "         -h FILE  copy the header from FILE [default is 1st input file]\n");
         fprintf(samtools_stderr, "         -o FILE  output BAM/CRAM\n");
         fprintf(samtools_stderr, "         --no-PG  do not add a PG line\n");
-        sam_global_opt_help(samtools_stderr, "--..-@-.");
-        return 1;
+        fprintf(samtools_stderr, "\nCRAM only options for filtering:\n");
+        fprintf(samtools_stderr, "         -r REG   filter to region REG.\n");
+        fprintf(samtools_stderr, "                  REG can also be #:cstart-cend for specific container numbers\n");
+        fprintf(samtools_stderr, "         -p N/M   Specify part N of M (where N is 1 to M inclusive)\n");
+        fprintf(samtools_stderr, "         -f       Fast mode: don't filter containers to exactly match region\n");
+        fprintf(samtools_stderr, "         -q       Query the total number of indexed containers\n");
+        fprintf(samtools_stderr, "\nStandard options:\n");
+        sam_global_opt_help(samtools_stderr, "---.---.");
+        ret = 1;
+        goto end;
     }
 
     in = sam_open(infns[0], "r");
     if (!in) {
         print_error_errno("cat", "failed to open file '%s'", infns[0]);
-        return 1;
+        ret = 1;
+        goto end;
     }
 
     switch (hts_get_format(in)->format) {
     case bam:
-        sam_close(in);
-        if (bam_cat(infns_size+nargv_fns, infns, h, outfn? outfn : "-", arg_list, no_pg) < 0)
+        if (bam_cat(in, infns_size+nargv_fns, infns, h, outfn? outfn : "-", arg_list, no_pg) < 0)
             ret = 1;
         break;
 
     case cram:
-        sam_close(in);
-        if (cram_cat(infns_size+nargv_fns, infns, h, outfn? outfn : "-", &ga, arg_list, no_pg) < 0)
+        if (query_ncont) {
+            if (cram_query_ncont(infns_size+nargv_fns, infns, reg) < 0)
+                ret = 1;
+        } else {
+            if (cram_cat(in, infns_size+nargv_fns, infns, h,
+                         outfn? outfn : "-", &ga, arg_list, no_pg, reg,
+                         part, fast_mode) < 0)
             ret = 1;
+        }
         break;
 
     default:
-        sam_close(in);
         fprintf(samtools_stderr, "[%s] ERROR: input is not BAM or CRAM\n", __func__);
-        return 1;
+        ret = 1;
     }
+    sam_close(in);
 
  end:
     if (infns_size > 0) {
@@ -584,6 +983,7 @@ int main_cat(int argc, char *argv[])
     free(arg_list);
     if (h)
         sam_hdr_destroy(h);
+    sam_global_args_free(&ga);
 
     return ret;
 }
index 3cbb24fa7d96afffc791803215a4b752d5af9ee9..8572e0f039011405d6a4d5c41fb9f8eadc3ad369 100644 (file)
@@ -1,7 +1,7 @@
 /*  bam_consensus.c -- consensus subcommand.
 
     Copyright (C) 1998-2001,2003 Medical Research Council (Gap4/5 source)
-    Copyright (C) 2003-2005,2007-2023 Genome Research Ltd.
+    Copyright (C) 2003-2005,2007-2024 Genome Research Ltd.
 
     Author: James Bonfield <jkb@sanger.ac.uk>
 
@@ -1917,8 +1917,8 @@ static int calculate_consensus_simple(const pileup_t *plp,
     // Ignore ambiguous bases in seq for now, so we don't treat R, Y, etc
     // as part of one base and part another.  Based on BAM seqi values.
     // We also use freq[16] as "*" for gap.
-    int freq[17] = {0};  // base frequency, aka depth
-    int score[17] = {0}; // summation of base qualities
+    int      freq[17]  = {0}; // base frequency, aka depth
+    uint64_t score[17] = {0}; // summation of base qualities
 
     // Accumulate
     for (; plp; plp = plp->next) {
@@ -1959,13 +1959,13 @@ static int calculate_consensus_simple(const pileup_t *plp,
     }
 
     // Total usable depth
-    int tscore = 0;
+    uint64_t tscore = 0;
     for (i = 0; i < 5; i++)
         tscore += score[1<<i];
 
     // Best and second best potential calls
-    int call1  = 15, call2 = 15;
-    int score1 = 0,  score2 = 0;
+    int      call1  = 15, call2  = 15;
+    uint64_t score1 = 0,  score2 = 0;
     for (i = 0; i < 5; i++) {
         int c = 1<<i; // A C G T *
         if (score1 < score[c]) {
@@ -1980,8 +1980,8 @@ static int calculate_consensus_simple(const pileup_t *plp,
     }
 
     // Work out which best and second best are usable as a call
-    int used_score = score1;
-    int used_base  = call1;
+    uint64_t used_score = score1;
+    int      used_base  = call1;
     if (score2 >= opts->het_fract * score1 && opts->ambig) {
         used_base  |= call2;
         used_score += score2;
@@ -2083,7 +2083,10 @@ static int basic_pileup(void *cd, samFile *fp, sam_hdr_t *h, pileup_t *p,
         calculate_consensus_gap5m(pos, opts->use_mqual ? CONS_MQUAL : 0,
                                   depth, p, opts, &cons, opts->default_qual,
                                   &cons_prob_recall, &cons_prob_precise);
-        if (cons.het_logodd > 0 && opts->ambig) {
+        if (cons.depth < opts->min_depth) {
+            cb = 'N';
+            cq = 0;
+        } else if (cons.het_logodd > 0 && opts->ambig) {
             cb = "AMRWa" // 5x5 matrix with ACGT* per row / col
                  "MCSYc"
                  "RSGKg"
@@ -2228,14 +2231,17 @@ static int basic_fasta(void *cd, samFile *fp, sam_hdr_t *h, pileup_t *p,
         calculate_consensus_gap5m(pos, opts->use_mqual ? CONS_MQUAL : 0,
                                   depth, p, opts, &cons, opts->default_qual,
                                   &cons_prob_recall, &cons_prob_precise);
-        if (cons.het_logodd > 0 && opts->ambig) {
+        if (cons.depth < opts->min_depth) {
+            cb = 'N';
+            cq = 0;
+        } else if (cons.het_logodd > 0 && opts->ambig) {
             cb = "AMRWa" // 5x5 matrix with ACGT* per row / col
                  "MCSYc"
                  "RSGKg"
                  "WYKTt"
                  "acgt*"[cons.het_call];
             cq = cons.het_logodd;
-        } else{
+        } else {
             cb = "ACGT*"[cons.call];
             cq = cons.phred;
         }
@@ -2319,10 +2325,10 @@ static void usage_exit(FILE *fp, int exit_status) {
     fprintf(fp, "  --show-ins yes/no     Whether to show insertions [yes]\n");
     fprintf(fp, "  --mark-ins            Add '+' before every inserted base/qual [off]\n");
     fprintf(fp, "  -A, --ambig           Enable IUPAC ambiguity codes [off]\n");
+    fprintf(fp, "  -d, --min-depth INT   Minimum depth of INT [1]\n");
     fprintf(fp, "\nFor simple consensus mode:\n");
     fprintf(fp, "  -q, --(no-)use-qual   Use quality values in calculation [off]\n");
     fprintf(fp, "  -c, --call-fract INT  At least INT portion of bases must agree [0.75]\n");
-    fprintf(fp, "  -d, --min-depth INT   Minimum depth of INT [2]\n");
     fprintf(fp, "  -H, --het-fract INT   Minimum fraction of 2nd-most to most common base [0.15]\n");
     fprintf(fp, "\nFor default \"Bayesian\" consensus mode:\n");
     fprintf(fp, "  -C, --cutoff C        Consensus cutoff quality C [10]\n");
index b090a9a565a95768460b2f923d184128909972c8..9c73233df01be4cd0a017b1f3945c52ebda3ed08 100644 (file)
@@ -3,7 +3,7 @@
 /*  bam_consensus.c -- consensus subcommand.
 
     Copyright (C) 1998-2001,2003 Medical Research Council (Gap4/5 source)
-    Copyright (C) 2003-2005,2007-2023 Genome Research Ltd.
+    Copyright (C) 2003-2005,2007-2024 Genome Research Ltd.
 
     Author: James Bonfield <jkb@sanger.ac.uk>
 
@@ -1919,8 +1919,8 @@ static int calculate_consensus_simple(const pileup_t *plp,
     // Ignore ambiguous bases in seq for now, so we don't treat R, Y, etc
     // as part of one base and part another.  Based on BAM seqi values.
     // We also use freq[16] as "*" for gap.
-    int freq[17] = {0};  // base frequency, aka depth
-    int score[17] = {0}; // summation of base qualities
+    int      freq[17]  = {0}; // base frequency, aka depth
+    uint64_t score[17] = {0}; // summation of base qualities
 
     // Accumulate
     for (; plp; plp = plp->next) {
@@ -1961,13 +1961,13 @@ static int calculate_consensus_simple(const pileup_t *plp,
     }
 
     // Total usable depth
-    int tscore = 0;
+    uint64_t tscore = 0;
     for (i = 0; i < 5; i++)
         tscore += score[1<<i];
 
     // Best and second best potential calls
-    int call1  = 15, call2 = 15;
-    int score1 = 0,  score2 = 0;
+    int      call1  = 15, call2  = 15;
+    uint64_t score1 = 0,  score2 = 0;
     for (i = 0; i < 5; i++) {
         int c = 1<<i; // A C G T *
         if (score1 < score[c]) {
@@ -1982,8 +1982,8 @@ static int calculate_consensus_simple(const pileup_t *plp,
     }
 
     // Work out which best and second best are usable as a call
-    int used_score = score1;
-    int used_base  = call1;
+    uint64_t used_score = score1;
+    int      used_base  = call1;
     if (score2 >= opts->het_fract * score1 && opts->ambig) {
         used_base  |= call2;
         used_score += score2;
@@ -2085,7 +2085,10 @@ static int basic_pileup(void *cd, samFile *fp, sam_hdr_t *h, pileup_t *p,
         calculate_consensus_gap5m(pos, opts->use_mqual ? CONS_MQUAL : 0,
                                   depth, p, opts, &cons, opts->default_qual,
                                   &cons_prob_recall, &cons_prob_precise);
-        if (cons.het_logodd > 0 && opts->ambig) {
+        if (cons.depth < opts->min_depth) {
+            cb = 'N';
+            cq = 0;
+        } else if (cons.het_logodd > 0 && opts->ambig) {
             cb = "AMRWa" // 5x5 matrix with ACGT* per row / col
                  "MCSYc"
                  "RSGKg"
@@ -2230,14 +2233,17 @@ static int basic_fasta(void *cd, samFile *fp, sam_hdr_t *h, pileup_t *p,
         calculate_consensus_gap5m(pos, opts->use_mqual ? CONS_MQUAL : 0,
                                   depth, p, opts, &cons, opts->default_qual,
                                   &cons_prob_recall, &cons_prob_precise);
-        if (cons.het_logodd > 0 && opts->ambig) {
+        if (cons.depth < opts->min_depth) {
+            cb = 'N';
+            cq = 0;
+        } else if (cons.het_logodd > 0 && opts->ambig) {
             cb = "AMRWa" // 5x5 matrix with ACGT* per row / col
                  "MCSYc"
                  "RSGKg"
                  "WYKTt"
                  "acgt*"[cons.het_call];
             cq = cons.het_logodd;
-        } else{
+        } else {
             cb = "ACGT*"[cons.call];
             cq = cons.phred;
         }
@@ -2321,10 +2327,10 @@ static void usage_exit(FILE *fp, int exit_status) {
     fprintf(fp, "  --show-ins yes/no     Whether to show insertions [yes]\n");
     fprintf(fp, "  --mark-ins            Add '+' before every inserted base/qual [off]\n");
     fprintf(fp, "  -A, --ambig           Enable IUPAC ambiguity codes [off]\n");
+    fprintf(fp, "  -d, --min-depth INT   Minimum depth of INT [1]\n");
     fprintf(fp, "\nFor simple consensus mode:\n");
     fprintf(fp, "  -q, --(no-)use-qual   Use quality values in calculation [off]\n");
     fprintf(fp, "  -c, --call-fract INT  At least INT portion of bases must agree [0.75]\n");
-    fprintf(fp, "  -d, --min-depth INT   Minimum depth of INT [2]\n");
     fprintf(fp, "  -H, --het-fract INT   Minimum fraction of 2nd-most to most common base [0.15]\n");
     fprintf(fp, "\nFor default \"Bayesian\" consensus mode:\n");
     fprintf(fp, "  -C, --cutoff C        Consensus cutoff quality C [10]\n");
index e4701b1e78710937261f34407b2d824aa61a30b3..fadcccb761b2ae830b9372493e479a0e3a8ce60c 100644 (file)
@@ -1,6 +1,6 @@
 /*  bam_fastq.c -- FASTA and FASTQ file generation
 
-    Copyright (C) 2009-2017, 2019-2020, 2023 Genome Research Ltd.
+    Copyright (C) 2009-2017, 2019-2020, 2023-2024 Genome Research Ltd.
     Portions copyright (C) 2009, 2011, 2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -40,9 +40,13 @@ DEALINGS IN THE SOFTWARE.  */
 #include "htslib/kstring.h"
 #include "htslib/bgzf.h"
 #include "htslib/thread_pool.h"
+#include "htslib/khash.h"
 #include "samtools.h"
 #include "sam_opts.h"
 
+KHASH_SET_INIT_STR(str)
+typedef khash_t(str) strhash_t;
+
 #define DEFAULT_BARCODE_TAG "BC"
 #define DEFAULT_QUALITY_TAG "QT"
 #define INDEX_SEPARATOR "+"
@@ -67,6 +71,8 @@ static void bam2fq_usage(FILE *to, const char *command)
 "               paired reads will be written to the -1 and -2 files.\n"
 "  -d, --tag TAG[:VAL]\n"
 "               only include reads containing TAG, optionally with value VAL\n"
+"  -D, --tag-file STR:FILE\n"
+"               only include reads containing TAG, with a value listed in FILE\n"
 "  -f, --require-flags INT\n"
 "               only include reads with all  of the FLAGs in INT present [0]\n"       //   F&x == x
 "  -F, --excl[ude]-flags INT\n"
@@ -150,9 +156,7 @@ typedef struct bam2fq_opts {
     char *extra_tags;
     char compression_level;
     const char *filter_tag;       // -d opt
-    const char *filter_value_str;
-    int64_t filter_value_int;
-    float filter_value_flt;
+    strhash_t *filter_tag_vals;
 } bam2fq_opts_t;
 
 typedef struct bam2fq_state {
@@ -171,6 +175,45 @@ typedef struct bam2fq_state {
     htsThreadPool p;
 } bam2fq_state_t;
 
+// Adds a single tag value to the filter tag value hash
+static int add_tag_value(bam2fq_opts_t *opts, char *val) {
+    if (!opts->filter_tag_vals) {
+        if (!(opts->filter_tag_vals = kh_init(str)))
+            return -1;
+    }
+
+    if (!(val = strdup(val)))
+        return -1;
+
+    int ret = 0;
+    kh_put(str, opts->filter_tag_vals, val, &ret);
+    if (ret <= 0)
+        free(val);
+
+    return ret < 0 ? -1 : 0;
+}
+
+// Adds multiple values, listed in a file
+static int add_tag_file(bam2fq_opts_t *opts, char *fn) {
+    FILE *fp;
+    kstring_t ks = {0,0};
+
+    if (!(fp = fopen(fn, "r"))) {
+        print_error_errno("fastq", "failed to open \"%s\" for reading", fn);
+        return -1;
+    }
+
+    while (ks.l = 0, kgetline(&ks, (kgets_func *)fgets, fp) >= 0) {
+        if (add_tag_value(opts, ks.s) < 0) {
+            ks_free(&ks);
+            return -1;
+        }
+    }
+
+    ks_free(&ks);
+    return 0;
+}
+
 static readpart which_readpart(const bam1_t *b)
 {
     if ((b->core.flag & BAM_FREAD1) && !(b->core.flag & BAM_FREAD2)) {
@@ -184,6 +227,14 @@ static readpart which_readpart(const bam1_t *b)
 
 static void free_opts(bam2fq_opts_t *opts)
 {
+    if (opts->filter_tag_vals) {
+        khint_t k;
+        for (k = 0; k < kh_end(opts->filter_tag_vals); k++)
+            if (kh_exist(opts->filter_tag_vals, k))
+                free((char *)kh_key(opts->filter_tag_vals, k));
+
+        kh_destroy(str, opts->filter_tag_vals);
+    }
     free(opts);
 }
 
@@ -230,9 +281,10 @@ static bool parse_opts(int argc, char *argv[], bam2fq_opts_t** opts_out)
         {"barcode-tag", required_argument, NULL, 'b'},
         {"quality-tag", required_argument, NULL, 'q'},
         {"tag", required_argument, NULL, 'd'},
+        {"tag-file", required_argument, NULL, 'D'},
         { NULL, 0, NULL, 0 }
     };
-    while ((c = getopt_long(argc, argv, "0:1:2:o:f:F:G:niNOs:c:tT:v:@:d:",
+    while ((c = getopt_long(argc, argv, "0:1:2:o:f:F:G:niNOs:c:tT:v:@:d:D:",
                             lopts, NULL)) > 0) {
         switch (c) {
             case 'b': opts->barcode_tag = optarg; break;
@@ -276,10 +328,44 @@ static bool parse_opts(int argc, char *argv[], bam2fq_opts_t** opts_out)
                     return false;
                 }
 
+                if (opts->filter_tag && memcmp(opts->filter_tag, optarg, 2)) {
+                    print_error("fastq", "Different tag type specified "
+                                "to before");
+                    free_opts(opts);
+                    return false;
+                }
+
+                if (strlen(optarg) >= 3)
+                    add_tag_value(opts, optarg+3);
+                opts->filter_tag = optarg;
+                break;
+
+            case 'D':
+                // Allow ";" as delimiter besides ":" to support MinGW CLI POSIX
+                // path translation as described at:
+                // http://www.mingw.org/wiki/Posix_path_conversion
+                if (strlen(optarg) < 4
+                    || (optarg[2] != ':' && optarg[2] != ';')) {
+                    print_error("view", "Invalid \"tag:file\" option: \"%s\"",
+                                optarg);
+                    free_opts(opts);
+                    return false;
+                }
+
+                if (opts->filter_tag && memcmp(opts->filter_tag, optarg, 2)) {
+                    print_error("fastq", "Different tag type specified "
+                                "to before");
+                    free_opts(opts);
+                    return false;
+                }
+
+                if (strlen(optarg) >= 3) {
+                    if (add_tag_file(opts, optarg+3) < 0) {
+                        free_opts(opts);
+                        return false;
+                    }
+                }
                 opts->filter_tag = optarg;
-                opts->filter_value_str = strlen(optarg) > 2 ? optarg+3 : NULL;
-                opts->filter_value_int = INT64_MAX; // fill out later
-                opts->filter_value_flt = FLT_MAX;
                 break;
 
             case '?':
@@ -630,45 +716,43 @@ static inline bool filter_it_out(const bam1_t *b, const bam2fq_state_t *state,
         if (!s)
             return true;
 
-        if (opts->filter_value_str) {
+        if (opts->filter_tag_vals) {
+            char t[32], *val = t;
             switch (*s) {
             case 'i': case 'I':
             case 's': case 'S':
             case 'c': case 'C':
-                if (opts->filter_value_int == INT64_MAX)
-                    // cache integer conversion for repeated use
-                    opts->filter_value_int =
-                        strtoll(opts->filter_value_str, NULL, 0);
-                if (opts->filter_value_int != bam_aux2i(s))
+                if (snprintf(t, 32, "%"PRId64, bam_aux2i(s)) <= 0)
                     return true;
                 break;
 
             case 'f':
-                if (opts->filter_value_flt == FLT_MAX)
-                    opts->filter_value_flt = atof(opts->filter_value_str);
                 // Comparing floats is hard.
                 // Eg (double)0.1 - (double)0.1f is -1.5e-9.
                 // Given BAM binary encoding is float however, just keep it.
                 // This means rounding errors will (hopefully) always be the
                 // same and basic equality still works.
-                if (opts->filter_value_flt != (float)bam_aux2f(s))
+                if (snprintf(t, 32, "%f", (float)bam_aux2f(s)) <= 0)
                     return true;
                 break;
 
             case 'A':
-                if (s[1] != *opts->filter_value_str)
-                    return true;
+                t[0] = s[1];
+                t[1] = 0;
                 break;
 
             case 'Z': case 'H':
-                if (strcmp((char *)s+1, opts->filter_value_str) != 0)
-                    return true;
+                val = (char *)s+1;
                 break;
 
             default:
                 // Anything unsupported fails the filter match too.
                 return true;
             }
+
+            khint_t k = kh_get(str, opts->filter_tag_vals, val);
+            if (k == kh_end(opts->filter_tag_vals))
+                return 1; // tag value not found
         }
     }
 
@@ -764,8 +848,13 @@ int output_index(bam1_t *b1, bam1_t *b2, bam2fq_state_t *state,
         }
 
         char *bc_end = bc, *qt_end = qt;
-        while (len ? *bc_end && rem-- : isalpha(*bc_end))
-            bc_end++, qt_end += qt != NULL;
+        if (qt) {
+            while (len ? *bc_end && rem-- : isalpha(*bc_end))
+                bc_end++, qt_end++;
+        } else {
+            while (len ? *bc_end && rem-- : isalpha(*bc_end))
+                bc_end++;
+        }
 
         switch (fc) {
         case 'n':
index cd8fa2757554205d4a0e2fbf1e607d9c208eaf8f..627f741af6ff5fc42f5b32e5bd7cc8442e4b052e 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  bam_fastq.c -- FASTA and FASTQ file generation
 
-    Copyright (C) 2009-2017, 2019-2020, 2023 Genome Research Ltd.
+    Copyright (C) 2009-2017, 2019-2020, 2023-2024 Genome Research Ltd.
     Portions copyright (C) 2009, 2011, 2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -42,9 +42,13 @@ DEALINGS IN THE SOFTWARE.  */
 #include "htslib/kstring.h"
 #include "htslib/bgzf.h"
 #include "htslib/thread_pool.h"
+#include "htslib/khash.h"
 #include "samtools.h"
 #include "sam_opts.h"
 
+KHASH_SET_INIT_STR(str)
+typedef khash_t(str) strhash_t;
+
 #define DEFAULT_BARCODE_TAG "BC"
 #define DEFAULT_QUALITY_TAG "QT"
 #define INDEX_SEPARATOR "+"
@@ -69,6 +73,8 @@ static void bam2fq_usage(FILE *to, const char *command)
 "               paired reads will be written to the -1 and -2 files.\n"
 "  -d, --tag TAG[:VAL]\n"
 "               only include reads containing TAG, optionally with value VAL\n"
+"  -D, --tag-file STR:FILE\n"
+"               only include reads containing TAG, with a value listed in FILE\n"
 "  -f, --require-flags INT\n"
 "               only include reads with all  of the FLAGs in INT present [0]\n"       //   F&x == x
 "  -F, --excl[ude]-flags INT\n"
@@ -152,9 +158,7 @@ typedef struct bam2fq_opts {
     char *extra_tags;
     char compression_level;
     const char *filter_tag;       // -d opt
-    const char *filter_value_str;
-    int64_t filter_value_int;
-    float filter_value_flt;
+    strhash_t *filter_tag_vals;
 } bam2fq_opts_t;
 
 typedef struct bam2fq_state {
@@ -173,6 +177,45 @@ typedef struct bam2fq_state {
     htsThreadPool p;
 } bam2fq_state_t;
 
+// Adds a single tag value to the filter tag value hash
+static int add_tag_value(bam2fq_opts_t *opts, char *val) {
+    if (!opts->filter_tag_vals) {
+        if (!(opts->filter_tag_vals = kh_init(str)))
+            return -1;
+    }
+
+    if (!(val = strdup(val)))
+        return -1;
+
+    int ret = 0;
+    kh_put(str, opts->filter_tag_vals, val, &ret);
+    if (ret <= 0)
+        free(val);
+
+    return ret < 0 ? -1 : 0;
+}
+
+// Adds multiple values, listed in a file
+static int add_tag_file(bam2fq_opts_t *opts, char *fn) {
+    FILE *fp;
+    kstring_t ks = {0,0};
+
+    if (!(fp = fopen(fn, "r"))) {
+        print_error_errno("fastq", "failed to open \"%s\" for reading", fn);
+        return -1;
+    }
+
+    while (ks.l = 0, kgetline(&ks, (kgets_func *)fgets, fp) >= 0) {
+        if (add_tag_value(opts, ks.s) < 0) {
+            ks_free(&ks);
+            return -1;
+        }
+    }
+
+    ks_free(&ks);
+    return 0;
+}
+
 static readpart which_readpart(const bam1_t *b)
 {
     if ((b->core.flag & BAM_FREAD1) && !(b->core.flag & BAM_FREAD2)) {
@@ -186,6 +229,14 @@ static readpart which_readpart(const bam1_t *b)
 
 static void free_opts(bam2fq_opts_t *opts)
 {
+    if (opts->filter_tag_vals) {
+        khint_t k;
+        for (k = 0; k < kh_end(opts->filter_tag_vals); k++)
+            if (kh_exist(opts->filter_tag_vals, k))
+                free((char *)kh_key(opts->filter_tag_vals, k));
+
+        kh_destroy(str, opts->filter_tag_vals);
+    }
     free(opts);
 }
 
@@ -232,9 +283,10 @@ static bool parse_opts(int argc, char *argv[], bam2fq_opts_t** opts_out)
         {"barcode-tag", required_argument, NULL, 'b'},
         {"quality-tag", required_argument, NULL, 'q'},
         {"tag", required_argument, NULL, 'd'},
+        {"tag-file", required_argument, NULL, 'D'},
         { NULL, 0, NULL, 0 }
     };
-    while ((c = getopt_long(argc, argv, "0:1:2:o:f:F:G:niNOs:c:tT:v:@:d:",
+    while ((c = getopt_long(argc, argv, "0:1:2:o:f:F:G:niNOs:c:tT:v:@:d:D:",
                             lopts, NULL)) > 0) {
         switch (c) {
             case 'b': opts->barcode_tag = optarg; break;
@@ -278,10 +330,44 @@ static bool parse_opts(int argc, char *argv[], bam2fq_opts_t** opts_out)
                     return false;
                 }
 
+                if (opts->filter_tag && memcmp(opts->filter_tag, optarg, 2)) {
+                    print_error("fastq", "Different tag type specified "
+                                "to before");
+                    free_opts(opts);
+                    return false;
+                }
+
+                if (strlen(optarg) >= 3)
+                    add_tag_value(opts, optarg+3);
+                opts->filter_tag = optarg;
+                break;
+
+            case 'D':
+                // Allow ";" as delimiter besides ":" to support MinGW CLI POSIX
+                // path translation as described at:
+                // http://www.mingw.org/wiki/Posix_path_conversion
+                if (strlen(optarg) < 4
+                    || (optarg[2] != ':' && optarg[2] != ';')) {
+                    print_error("view", "Invalid \"tag:file\" option: \"%s\"",
+                                optarg);
+                    free_opts(opts);
+                    return false;
+                }
+
+                if (opts->filter_tag && memcmp(opts->filter_tag, optarg, 2)) {
+                    print_error("fastq", "Different tag type specified "
+                                "to before");
+                    free_opts(opts);
+                    return false;
+                }
+
+                if (strlen(optarg) >= 3) {
+                    if (add_tag_file(opts, optarg+3) < 0) {
+                        free_opts(opts);
+                        return false;
+                    }
+                }
                 opts->filter_tag = optarg;
-                opts->filter_value_str = strlen(optarg) > 2 ? optarg+3 : NULL;
-                opts->filter_value_int = INT64_MAX; // fill out later
-                opts->filter_value_flt = FLT_MAX;
                 break;
 
             case '?':
@@ -632,45 +718,43 @@ static inline bool filter_it_out(const bam1_t *b, const bam2fq_state_t *state,
         if (!s)
             return true;
 
-        if (opts->filter_value_str) {
+        if (opts->filter_tag_vals) {
+            char t[32], *val = t;
             switch (*s) {
             case 'i': case 'I':
             case 's': case 'S':
             case 'c': case 'C':
-                if (opts->filter_value_int == INT64_MAX)
-                    // cache integer conversion for repeated use
-                    opts->filter_value_int =
-                        strtoll(opts->filter_value_str, NULL, 0);
-                if (opts->filter_value_int != bam_aux2i(s))
+                if (snprintf(t, 32, "%"PRId64, bam_aux2i(s)) <= 0)
                     return true;
                 break;
 
             case 'f':
-                if (opts->filter_value_flt == FLT_MAX)
-                    opts->filter_value_flt = atof(opts->filter_value_str);
                 // Comparing floats is hard.
                 // Eg (double)0.1 - (double)0.1f is -1.5e-9.
                 // Given BAM binary encoding is float however, just keep it.
                 // This means rounding errors will (hopefully) always be the
                 // same and basic equality still works.
-                if (opts->filter_value_flt != (float)bam_aux2f(s))
+                if (snprintf(t, 32, "%f", (float)bam_aux2f(s)) <= 0)
                     return true;
                 break;
 
             case 'A':
-                if (s[1] != *opts->filter_value_str)
-                    return true;
+                t[0] = s[1];
+                t[1] = 0;
                 break;
 
             case 'Z': case 'H':
-                if (strcmp((char *)s+1, opts->filter_value_str) != 0)
-                    return true;
+                val = (char *)s+1;
                 break;
 
             default:
                 // Anything unsupported fails the filter match too.
                 return true;
             }
+
+            khint_t k = kh_get(str, opts->filter_tag_vals, val);
+            if (k == kh_end(opts->filter_tag_vals))
+                return 1; // tag value not found
         }
     }
 
@@ -766,8 +850,13 @@ int output_index(bam1_t *b1, bam1_t *b2, bam2fq_state_t *state,
         }
 
         char *bc_end = bc, *qt_end = qt;
-        while (len ? *bc_end && rem-- : isalpha(*bc_end))
-            bc_end++, qt_end += qt != NULL;
+        if (qt) {
+            while (len ? *bc_end && rem-- : isalpha(*bc_end))
+                bc_end++, qt_end++;
+        } else {
+            while (len ? *bc_end && rem-- : isalpha(*bc_end))
+                bc_end++;
+        }
 
         switch (fc) {
         case 'n':
index 079e04bf6a841682aec35a6790b34a636179fefb..6a3b2585eb8d3573b2a6234b8b889446d07f390b 100644 (file)
@@ -4,7 +4,7 @@
  *   samtools import a_1.fq a_2.fq
  *   samtools import a_interleaved.fq
  *
- * Copyright (C) 2020-2021, 2023 Genome Research Ltd.
+ * Copyright (C) 2020-2021, 2023-2024 Genome Research Ltd.
  *
  * Author: James Bonfield <jkb@sanger.ac.uk>
  */
@@ -63,6 +63,7 @@ static int usage(FILE *fp, int exit_status) {
     fprintf(fp, "  -u           Uncompressed output\n");
     fprintf(fp, "  --order TAG  Store Nth record count in TAG\n");
     fprintf(fp, "\n");
+    fprintf(fp, "      --no-PG  Do not add a PG line\n");
     sam_global_opt_help(fp, "-.O.-@--");
 
     fprintf(fp, "\nA single fastq file will be interpreted as -s, -0 or -1 depending on\n");
@@ -159,7 +160,7 @@ static int import_fastq(int argc, char **argv, opts_t *opts) {
     if (argc == 1)
         opts->fn[FQ_SINGLE] = argv[0];
     else
-        for (i = 0; i < 4; i++)
+        for (i = 0; i < 2; i++)
             if (argc > i)
                 opts->fn[FQ_R1+i] = argv[i];
 
@@ -259,6 +260,25 @@ static int import_fastq(int argc, char **argv, opts_t *opts) {
         goto err;
     }
 
+    if (!opts->no_pg) {
+        char *arg_list;
+        if (!(arg_list = stringify_argv(argc+1+optind, argv-1-optind))) {
+            print_error("view", "failed to create arg_list");
+            goto err;
+        }
+        if (sam_hdr_add_pg(hdr_out, "samtools",
+                           "VN", samtools_version(),
+                           arg_list ? "CL" : NULL,
+                           arg_list ? arg_list : NULL,
+                           NULL)) {
+            fprintf(stderr, "Failed to add PG line to the header");
+            free(arg_list);
+            goto err;
+        }
+
+        free(arg_list);
+    }
+
     // Read group
     if (opts->rg_line) {
         if (*opts->rg_line != '@')
@@ -290,7 +310,7 @@ static int import_fastq(int argc, char **argv, opts_t *opts) {
 
 
     // Interleave / combine from n files (ids[0..n-1]).
-    int res;
+    int res = 0;
     int eof = 0;
     do {
         idx_seq.l = idx_qual.l = 0;
index f16a7811fc4718a9375d9657804b2eb9d56b39e3..6725844b1bcefbec1c0697b32e1f71f6fed3ae7c 100644 (file)
@@ -6,7 +6,7 @@
  *   samtools import a_1.fq a_2.fq
  *   samtools import a_interleaved.fq
  *
- * Copyright (C) 2020-2021, 2023 Genome Research Ltd.
+ * Copyright (C) 2020-2021, 2023-2024 Genome Research Ltd.
  *
  * Author: James Bonfield <jkb@sanger.ac.uk>
  */
@@ -65,6 +65,7 @@ static int usage(FILE *fp, int exit_status) {
     fprintf(fp, "  -u           Uncompressed output\n");
     fprintf(fp, "  --order TAG  Store Nth record count in TAG\n");
     fprintf(fp, "\n");
+    fprintf(fp, "      --no-PG  Do not add a PG line\n");
     sam_global_opt_help(fp, "-.O.-@--");
 
     fprintf(fp, "\nA single fastq file will be interpreted as -s, -0 or -1 depending on\n");
@@ -161,7 +162,7 @@ static int import_fastq(int argc, char **argv, opts_t *opts) {
     if (argc == 1)
         opts->fn[FQ_SINGLE] = argv[0];
     else
-        for (i = 0; i < 4; i++)
+        for (i = 0; i < 2; i++)
             if (argc > i)
                 opts->fn[FQ_R1+i] = argv[i];
 
@@ -261,6 +262,25 @@ static int import_fastq(int argc, char **argv, opts_t *opts) {
         goto err;
     }
 
+    if (!opts->no_pg) {
+        char *arg_list;
+        if (!(arg_list = stringify_argv(argc+1+optind, argv-1-optind))) {
+            print_error("view", "failed to create arg_list");
+            goto err;
+        }
+        if (sam_hdr_add_pg(hdr_out, "samtools",
+                           "VN", samtools_version(),
+                           arg_list ? "CL" : NULL,
+                           arg_list ? arg_list : NULL,
+                           NULL)) {
+            fprintf(samtools_stderr, "Failed to add PG line to the header");
+            free(arg_list);
+            goto err;
+        }
+
+        free(arg_list);
+    }
+
     // Read group
     if (opts->rg_line) {
         if (*opts->rg_line != '@')
@@ -292,7 +312,7 @@ static int import_fastq(int argc, char **argv, opts_t *opts) {
 
 
     // Interleave / combine from n files (ids[0..n-1]).
-    int res;
+    int res = 0;
     int eof = 0;
     do {
         idx_seq.l = idx_qual.l = 0;
index 0803f3e4244c3bc1247282b0ebe184cff8486621..a04de1a6ed1f7d5fbbf6311fe7fed278757f39b0 100644 (file)
@@ -1,6 +1,6 @@
 /*  bam_index.c -- index and idxstats subcommands.
 
-    Copyright (C) 2008-2011, 2013-2016, 2018, 2019, 2023  Genome Research Ltd.
+    Copyright (C) 2008-2011, 2013-2016, 2018, 2019, 2023-2024  Genome Research Ltd.
     Portions copyright (C) 2010 Broad Institute.
     Portions copyright (C) 2013 Peter Cock, The James Hutton Institute.
 
@@ -209,7 +209,8 @@ int slow_idxstats(samFile *fp, sam_hdr_t *header) {
 
 static void usage_exit(FILE *fp, int exit_status)
 {
-    fprintf(fp, "Usage: samtools idxstats [options] <in.bam>\n");
+    fprintf(fp, "Usage: samtools idxstats [options] <in.bam>\n"
+                "  -X           Include customized index file\n");
     sam_global_opt_help(fp, "-.---@-.");
     exit(exit_status);
 }
@@ -219,7 +220,8 @@ int bam_idxstats(int argc, char *argv[])
     hts_idx_t* idx;
     sam_hdr_t* header;
     samFile* fp;
-    int c;
+    int c, has_index_file = 0, file_names = 1;
+    char *index_name = NULL;
 
     sam_global_args ga = SAM_GLOBAL_ARGS_INIT;
     static const struct option lopts[] = {
@@ -227,16 +229,22 @@ int bam_idxstats(int argc, char *argv[])
         {NULL, 0, NULL, 0}
     };
 
-    while ((c = getopt_long(argc, argv, "@:", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "@:X", lopts, NULL)) >= 0) {
         switch (c) {
-        default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
-            /* else fall-through */
-        case '?':
-            usage_exit(stderr, EXIT_FAILURE);
+            case 'X': has_index_file=1; break;
+            default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
+                /* else fall-through */
+            case '?':
+                usage_exit(stderr, EXIT_FAILURE);
         }
     }
 
-    if (argc != optind+1) {
+    if (has_index_file) {
+        file_names = 2;
+        index_name = argv[optind + 1];
+    }
+
+    if (argc != optind + file_names) {
         if (argc == optind) usage_exit(stdout, EXIT_SUCCESS);
         else usage_exit(stderr, EXIT_FAILURE);
     }
@@ -262,7 +270,7 @@ int bam_idxstats(int argc, char *argv[])
             return 1;
         }
     } else {
-        idx = sam_index_load(fp, argv[optind]);
+        idx = sam_index_load2(fp, argv[optind], index_name);
         if (idx == NULL) {
             print_error("idxstats", "fail to load index for \"%s\", "
                         "reverting to slow method", argv[optind]);
index 3093c01382ff1e509b50281dc5007a78c3ad7a39..09d01d470865826bce9b90d2a35a346ac16e44cb 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  bam_index.c -- index and idxstats subcommands.
 
-    Copyright (C) 2008-2011, 2013-2016, 2018, 2019, 2023  Genome Research Ltd.
+    Copyright (C) 2008-2011, 2013-2016, 2018, 2019, 2023-2024  Genome Research Ltd.
     Portions copyright (C) 2010 Broad Institute.
     Portions copyright (C) 2013 Peter Cock, The James Hutton Institute.
 
@@ -211,7 +211,8 @@ int slow_idxstats(samFile *fp, sam_hdr_t *header) {
 
 static void usage_exit(FILE *fp, int exit_status)
 {
-    fprintf(fp, "Usage: samtools idxstats [options] <in.bam>\n");
+    fprintf(fp, "Usage: samtools idxstats [options] <in.bam>\n"
+                "  -X           Include customized index file\n");
     sam_global_opt_help(fp, "-.---@-.");
     samtools_exit(exit_status);
 }
@@ -221,7 +222,8 @@ int bam_idxstats(int argc, char *argv[])
     hts_idx_t* idx;
     sam_hdr_t* header;
     samFile* fp;
-    int c;
+    int c, has_index_file = 0, file_names = 1;
+    char *index_name = NULL;
 
     sam_global_args ga = SAM_GLOBAL_ARGS_INIT;
     static const struct option lopts[] = {
@@ -229,16 +231,22 @@ int bam_idxstats(int argc, char *argv[])
         {NULL, 0, NULL, 0}
     };
 
-    while ((c = getopt_long(argc, argv, "@:", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "@:X", lopts, NULL)) >= 0) {
         switch (c) {
-        default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
-            /* else fall-through */
-        case '?':
-            usage_exit(samtools_stderr, EXIT_FAILURE);
+            case 'X': has_index_file=1; break;
+            default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
+                /* else fall-through */
+            case '?':
+                usage_exit(samtools_stderr, EXIT_FAILURE);
         }
     }
 
-    if (argc != optind+1) {
+    if (has_index_file) {
+        file_names = 2;
+        index_name = argv[optind + 1];
+    }
+
+    if (argc != optind + file_names) {
         if (argc == optind) usage_exit(samtools_stdout, EXIT_SUCCESS);
         else usage_exit(samtools_stderr, EXIT_FAILURE);
     }
@@ -264,7 +272,7 @@ int bam_idxstats(int argc, char *argv[])
             return 1;
         }
     } else {
-        idx = sam_index_load(fp, argv[optind]);
+        idx = sam_index_load2(fp, argv[optind], index_name);
         if (idx == NULL) {
             print_error("idxstats", "fail to load index for \"%s\", "
                         "reverting to slow method", argv[optind]);
index 677a47f8283c19957f6dfc7da041f542af3fec55..17fedd58dd0b8a5de7fde185051e2198b4d9cfd4 100644 (file)
@@ -122,6 +122,7 @@ typedef struct {
     int opt;
     int beg;
     int end;
+    int len;
 } check_t;
 
 typedef struct {
@@ -875,7 +876,10 @@ static int is_optical_duplicate(md_param_t *param, bam1_t *ori, bam1_t *dup, lon
         return ret;
     }
 
-    if (strncmp(original + o_beg, duplicate + d_beg, o_end - o_beg) == 0) {
+    int o_len = o_end - o_beg;
+    int d_len = d_end - d_beg;
+
+    if ((o_len == d_len) && memcmp(original + o_beg, duplicate + d_beg, o_len) == 0) {
         long xdiff, ydiff;
 
         if (ox > dx) {
@@ -918,7 +922,10 @@ static int optical_duplicate_partial(md_param_t *param, const char *name, const
         return ret;
     }
 
-    if (strncmp(name + o_beg, duplicate + d_beg, o_end - o_beg) == 0) {
+    int o_len = o_end - o_beg;
+    int d_len = d_end - d_beg;
+
+    if ((o_len == d_len) && memcmp(name + o_beg, duplicate + d_beg, o_len) == 0) {
         // the initial parts match, look at the numbers
         long xdiff, ydiff;
 
@@ -945,6 +952,7 @@ static int optical_duplicate_partial(md_param_t *param, const char *name, const
     c->y = dy;
     c->beg = d_beg;
     c->end = d_end;
+    c->len = d_end - d_beg;
 
     return ret;
 }
@@ -1156,9 +1164,15 @@ static int check_chain_against_original(md_param_t *param, khash_t(duplicates) *
 }
 
 
-static int xcoord_sort(const void *a, const void *b) {
+static int chain_sort(const void *a, const void *b) {
     check_t *ac = (check_t *) a;
     check_t *bc = (check_t *) b;
+    int ret;
+
+    if ((ret = ac->len - bc->len))
+        return ret;
+    else if ((ret = memcmp(bam_get_qname(ac->b) + ac->beg, bam_get_qname(bc->b) + bc->beg, ac->len)))
+        return ret;
 
     return (ac->x - bc->x);
 }
@@ -1170,106 +1184,113 @@ static int check_duplicate_chain(md_param_t *param, khash_t(duplicates) *dup_has
     int ret = 0;
     size_t curr = 0;
 
-    qsort(list->c, list->length, sizeof(list->c[0]), xcoord_sort);
+    qsort(list->c, list->length, sizeof(list->c[0]), chain_sort);
 
     while (curr < list->length - 1) {
-        check_t *current = &list->c[curr];
-        size_t count = curr;
-        char *cur_name = bam_get_qname(current->b);
-        int current_paired = (current->b->core.flag & BAM_FPAIRED) && !(current->b->core.flag & BAM_FMUNMAP);
+        check_t *base = &list->c[curr];
+        char *base_name = bam_get_qname(base->b);
+        int end_name_match = curr;
 
-        while (++count < list->length && (list->c[count].x - current->x <= param->opt_dist)) {
-            // while close enough along the x coordinate
-            check_t *chk = &list->c[count];
+        // find the end of the matching name parts
+        while (++end_name_match < list->length) {
+            check_t *chk = &list->c[end_name_match];
 
-            if (current->opt && chk->opt)
-                continue;
+            if ((base->len == chk->len) && memcmp(base_name + base->beg, bam_get_qname(chk->b) + chk->beg, base->len) != 0)
+                break;
+        }
 
-            // if both are already optical duplicates there is no need to check again, otherwise...
+        while (curr < end_name_match) {
+            size_t count = curr;
+            check_t *current = &list->c[curr];
+            int current_paired = (current->b->core.flag & BAM_FPAIRED) && !(current->b->core.flag & BAM_FMUNMAP);
 
-            long ydiff;
+            while (++count < end_name_match && (list->c[count].x - current->x <= param->opt_dist)) {
+                // while close enough along the x coordinate
+                check_t *chk = &list->c[count];
 
-            if (current->y > chk->y) {
-                ydiff = current->y - chk->y;
-            } else {
-                ydiff = chk->y - current->y;
-            }
+                if (current->opt && chk->opt)
+                    continue;
 
-            if (ydiff > param->opt_dist)
-                continue;
+                long ydiff;
 
-            // the number are right, check the names
-            if (strncmp(cur_name + current->beg, bam_get_qname(chk->b) + chk->beg, current->end - current->beg) != 0)
-                continue;
+                if (current->y > chk->y) {
+                    ydiff = current->y - chk->y;
+                } else {
+                    ydiff = chk->y - current->y;
+                }
 
-            // optical duplicates
-            int chk_dup = 0;
-            int chk_paired = (chk->b->core.flag & BAM_FPAIRED) && !(chk->b->core.flag & BAM_FMUNMAP);
+                if (ydiff > param->opt_dist)
+                    continue;
 
-            if (current_paired != chk_paired) {
-                if (!chk_paired) {
-                    // chk is single vs pair, this is a dup.
-                    chk_dup = 1;
-                }
-            } else {
-                // do it by scores
-                int64_t cur_score, chk_score;
+                // optical duplicates
+                int chk_dup = 0;
+                int chk_paired = (chk->b->core.flag & BAM_FPAIRED) && !(chk->b->core.flag & BAM_FMUNMAP);
 
-                if ((current->b->core.flag & BAM_FQCFAIL) != (chk->b->core.flag & BAM_FQCFAIL)) {
-                    if (current->b->core.flag & BAM_FQCFAIL) {
-                        cur_score = 0;
-                        chk_score = 1;
-                    } else {
-                        cur_score = 1;
-                        chk_score = 0;
+                if (current_paired != chk_paired) {
+                    if (!chk_paired) {
+                        // chk is single vs pair, this is a dup.
+                        chk_dup = 1;
                     }
                 } else {
-                    cur_score = current->score;
-                    chk_score = chk->score;
+                    // do it by scores
+                    int64_t cur_score, chk_score;
 
-                    if (current_paired) {
-                        // they are pairs so add mate scores.
-                        chk_score += chk->mate_score;
-                        cur_score += current->mate_score;
+                    if ((current->b->core.flag & BAM_FQCFAIL) != (chk->b->core.flag & BAM_FQCFAIL)) {
+                        if (current->b->core.flag & BAM_FQCFAIL) {
+                            cur_score = 0;
+                            chk_score = 1;
+                        } else {
+                            cur_score = 1;
+                            chk_score = 0;
+                        }
+                    } else {
+                        cur_score = current->score;
+                        chk_score = chk->score;
+
+                        if (current_paired) {
+                            // they are pairs so add mate scores.
+                            chk_score += chk->mate_score;
+                            cur_score += current->mate_score;
+                        }
                     }
-                }
 
-                if (cur_score == chk_score) {
-                    if (strcmp(bam_get_qname(chk->b), cur_name) < 0) {
-                        chk_score++;
-                    } else {
-                        chk_score--;
+                    if (cur_score == chk_score) {
+                        if (strcmp(bam_get_qname(chk->b), bam_get_qname(current->b)) < 0) {
+                            chk_score++;
+                        } else {
+                            chk_score--;
+                        }
                     }
-                }
 
-                if (cur_score > chk_score) {
-                    chk_dup = 1;
+                    if (cur_score > chk_score) {
+                        chk_dup = 1;
+                    }
                 }
-            }
 
-            if (chk_dup) {
-                // the duplicate is the optical duplicate
-                if (!chk->opt) { // only change if not already an optical duplicate
-                    if (optical_retag(param, dup_hash, chk->b, chk_paired, stats)) {
-                        ret = -1;
-                        goto fail;
-                    }
+                if (chk_dup) {
+                    // the duplicate is the optical duplicate
+                    if (!chk->opt) { // only change if not already an optical duplicate
+                        if (optical_retag(param, dup_hash, chk->b, chk_paired, stats)) {
+                            ret = -1;
+                            goto fail;
+                        }
 
-                    chk->opt = 1;
-                }
-            } else {
-                if (!current->opt) {
-                    if (optical_retag(param, dup_hash, current->b, current_paired, stats)) {
-                        ret = -1;
-                        goto fail;
+                        chk->opt = 1;
                     }
+                } else {
+                    if (!current->opt) {
+                        if (optical_retag(param, dup_hash, current->b, current_paired, stats)) {
+                            ret = -1;
+                            goto fail;
+                        }
 
-                    current->opt = 1;
+                        current->opt = 1;
+                    }
                 }
             }
-        }
 
-        curr++;
+            curr++;
+        }
     }
 
  fail:
index e8fea3d2a1b4b94382256203be42eda78181aecf..cdf33c1a8524452f01925cf63eec4de319a8dc91 100644 (file)
@@ -124,6 +124,7 @@ typedef struct {
     int opt;
     int beg;
     int end;
+    int len;
 } check_t;
 
 typedef struct {
@@ -877,7 +878,10 @@ static int is_optical_duplicate(md_param_t *param, bam1_t *ori, bam1_t *dup, lon
         return ret;
     }
 
-    if (strncmp(original + o_beg, duplicate + d_beg, o_end - o_beg) == 0) {
+    int o_len = o_end - o_beg;
+    int d_len = d_end - d_beg;
+
+    if ((o_len == d_len) && memcmp(original + o_beg, duplicate + d_beg, o_len) == 0) {
         long xdiff, ydiff;
 
         if (ox > dx) {
@@ -920,7 +924,10 @@ static int optical_duplicate_partial(md_param_t *param, const char *name, const
         return ret;
     }
 
-    if (strncmp(name + o_beg, duplicate + d_beg, o_end - o_beg) == 0) {
+    int o_len = o_end - o_beg;
+    int d_len = d_end - d_beg;
+
+    if ((o_len == d_len) && memcmp(name + o_beg, duplicate + d_beg, o_len) == 0) {
         // the initial parts match, look at the numbers
         long xdiff, ydiff;
 
@@ -947,6 +954,7 @@ static int optical_duplicate_partial(md_param_t *param, const char *name, const
     c->y = dy;
     c->beg = d_beg;
     c->end = d_end;
+    c->len = d_end - d_beg;
 
     return ret;
 }
@@ -1158,9 +1166,15 @@ static int check_chain_against_original(md_param_t *param, khash_t(duplicates) *
 }
 
 
-static int xcoord_sort(const void *a, const void *b) {
+static int chain_sort(const void *a, const void *b) {
     check_t *ac = (check_t *) a;
     check_t *bc = (check_t *) b;
+    int ret;
+
+    if ((ret = ac->len - bc->len))
+        return ret;
+    else if ((ret = memcmp(bam_get_qname(ac->b) + ac->beg, bam_get_qname(bc->b) + bc->beg, ac->len)))
+        return ret;
 
     return (ac->x - bc->x);
 }
@@ -1172,106 +1186,113 @@ static int check_duplicate_chain(md_param_t *param, khash_t(duplicates) *dup_has
     int ret = 0;
     size_t curr = 0;
 
-    qsort(list->c, list->length, sizeof(list->c[0]), xcoord_sort);
+    qsort(list->c, list->length, sizeof(list->c[0]), chain_sort);
 
     while (curr < list->length - 1) {
-        check_t *current = &list->c[curr];
-        size_t count = curr;
-        char *cur_name = bam_get_qname(current->b);
-        int current_paired = (current->b->core.flag & BAM_FPAIRED) && !(current->b->core.flag & BAM_FMUNMAP);
+        check_t *base = &list->c[curr];
+        char *base_name = bam_get_qname(base->b);
+        int end_name_match = curr;
 
-        while (++count < list->length && (list->c[count].x - current->x <= param->opt_dist)) {
-            // while close enough along the x coordinate
-            check_t *chk = &list->c[count];
+        // find the end of the matching name parts
+        while (++end_name_match < list->length) {
+            check_t *chk = &list->c[end_name_match];
 
-            if (current->opt && chk->opt)
-                continue;
+            if ((base->len == chk->len) && memcmp(base_name + base->beg, bam_get_qname(chk->b) + chk->beg, base->len) != 0)
+                break;
+        }
 
-            // if both are already optical duplicates there is no need to check again, otherwise...
+        while (curr < end_name_match) {
+            size_t count = curr;
+            check_t *current = &list->c[curr];
+            int current_paired = (current->b->core.flag & BAM_FPAIRED) && !(current->b->core.flag & BAM_FMUNMAP);
 
-            long ydiff;
+            while (++count < end_name_match && (list->c[count].x - current->x <= param->opt_dist)) {
+                // while close enough along the x coordinate
+                check_t *chk = &list->c[count];
 
-            if (current->y > chk->y) {
-                ydiff = current->y - chk->y;
-            } else {
-                ydiff = chk->y - current->y;
-            }
+                if (current->opt && chk->opt)
+                    continue;
 
-            if (ydiff > param->opt_dist)
-                continue;
+                long ydiff;
 
-            // the number are right, check the names
-            if (strncmp(cur_name + current->beg, bam_get_qname(chk->b) + chk->beg, current->end - current->beg) != 0)
-                continue;
+                if (current->y > chk->y) {
+                    ydiff = current->y - chk->y;
+                } else {
+                    ydiff = chk->y - current->y;
+                }
 
-            // optical duplicates
-            int chk_dup = 0;
-            int chk_paired = (chk->b->core.flag & BAM_FPAIRED) && !(chk->b->core.flag & BAM_FMUNMAP);
+                if (ydiff > param->opt_dist)
+                    continue;
 
-            if (current_paired != chk_paired) {
-                if (!chk_paired) {
-                    // chk is single vs pair, this is a dup.
-                    chk_dup = 1;
-                }
-            } else {
-                // do it by scores
-                int64_t cur_score, chk_score;
+                // optical duplicates
+                int chk_dup = 0;
+                int chk_paired = (chk->b->core.flag & BAM_FPAIRED) && !(chk->b->core.flag & BAM_FMUNMAP);
 
-                if ((current->b->core.flag & BAM_FQCFAIL) != (chk->b->core.flag & BAM_FQCFAIL)) {
-                    if (current->b->core.flag & BAM_FQCFAIL) {
-                        cur_score = 0;
-                        chk_score = 1;
-                    } else {
-                        cur_score = 1;
-                        chk_score = 0;
+                if (current_paired != chk_paired) {
+                    if (!chk_paired) {
+                        // chk is single vs pair, this is a dup.
+                        chk_dup = 1;
                     }
                 } else {
-                    cur_score = current->score;
-                    chk_score = chk->score;
+                    // do it by scores
+                    int64_t cur_score, chk_score;
 
-                    if (current_paired) {
-                        // they are pairs so add mate scores.
-                        chk_score += chk->mate_score;
-                        cur_score += current->mate_score;
+                    if ((current->b->core.flag & BAM_FQCFAIL) != (chk->b->core.flag & BAM_FQCFAIL)) {
+                        if (current->b->core.flag & BAM_FQCFAIL) {
+                            cur_score = 0;
+                            chk_score = 1;
+                        } else {
+                            cur_score = 1;
+                            chk_score = 0;
+                        }
+                    } else {
+                        cur_score = current->score;
+                        chk_score = chk->score;
+
+                        if (current_paired) {
+                            // they are pairs so add mate scores.
+                            chk_score += chk->mate_score;
+                            cur_score += current->mate_score;
+                        }
                     }
-                }
 
-                if (cur_score == chk_score) {
-                    if (strcmp(bam_get_qname(chk->b), cur_name) < 0) {
-                        chk_score++;
-                    } else {
-                        chk_score--;
+                    if (cur_score == chk_score) {
+                        if (strcmp(bam_get_qname(chk->b), bam_get_qname(current->b)) < 0) {
+                            chk_score++;
+                        } else {
+                            chk_score--;
+                        }
                     }
-                }
 
-                if (cur_score > chk_score) {
-                    chk_dup = 1;
+                    if (cur_score > chk_score) {
+                        chk_dup = 1;
+                    }
                 }
-            }
 
-            if (chk_dup) {
-                // the duplicate is the optical duplicate
-                if (!chk->opt) { // only change if not already an optical duplicate
-                    if (optical_retag(param, dup_hash, chk->b, chk_paired, stats)) {
-                        ret = -1;
-                        goto fail;
-                    }
+                if (chk_dup) {
+                    // the duplicate is the optical duplicate
+                    if (!chk->opt) { // only change if not already an optical duplicate
+                        if (optical_retag(param, dup_hash, chk->b, chk_paired, stats)) {
+                            ret = -1;
+                            goto fail;
+                        }
 
-                    chk->opt = 1;
-                }
-            } else {
-                if (!current->opt) {
-                    if (optical_retag(param, dup_hash, current->b, current_paired, stats)) {
-                        ret = -1;
-                        goto fail;
+                        chk->opt = 1;
                     }
+                } else {
+                    if (!current->opt) {
+                        if (optical_retag(param, dup_hash, current->b, current_paired, stats)) {
+                            ret = -1;
+                            goto fail;
+                        }
 
-                    current->opt = 1;
+                        current->opt = 1;
+                    }
                 }
             }
-        }
 
-        curr++;
+            curr++;
+        }
     }
 
  fail:
index 1f2b576a7a0df1d31e13d128d518f0f54207d7d3..8fb02cf6427ac1d589c75ea61b668d7595a7e8b8 100644 (file)
@@ -1,6 +1,6 @@
 /*  bam_mate.c -- fix mate pairing information and clean up flags.
 
-    Copyright (C) 2009, 2011-2017, 2019, 2022 Genome Research Ltd.
+    Copyright (C) 2009, 2011-2017, 2019, 2022, 2024 Genome Research Ltd.
     Portions copyright (C) 2011 Broad Institute.
     Portions copyright (C) 2012 Peter Cock, The James Hutton Institute.
 
@@ -31,6 +31,7 @@ DEALINGS IN THE SOFTWARE.  */
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
+#include <ctype.h>
 #include "htslib/thread_pool.h"
 #include "sam_opts.h"
 #include "htslib/kstring.h"
@@ -90,7 +91,7 @@ static void bam_template_cigar(bam1_t *b1, bam1_t *b2, kstring_t *str)
  * single Reads:
  * -if pos == 0 (1 based), tid == -1, or UNMAPPED then set UNMAPPED, pos = 0,
  *  tid = -1
- * -clear bad flags (PAIRED, MREVERSE, PROPER_PAIR)
+ * -clear bad flags (MREVERSE, PROPER_PAIR)
  * -set mpos = 0 (1 based), mtid = -1 and isize = 0
  * -write to output
  * Paired Reads:
@@ -472,17 +473,495 @@ int bam_sanitize(sam_hdr_t *h, bam1_t *b, int flags) {
     return 0;
 }
 
+// Look for 3 tags in one pass, for efficiencies sake.
+// We also convert the draft tags Mm and Ml to MM and ML here.
+static inline void find_tags(bam1_t *b,
+                             char *t1, uint8_t **t1p,
+                             char *t2, uint8_t **t2p,
+                             char *t3, uint8_t **t3p) {
+    *t1p = *t2p = *t3p = NULL;
+    uint8_t *aux = bam_aux_first(b);
+
+    while (aux) {
+        if (aux[-2] == t1[0] && toupper(aux[-1]) == t1[1]) {
+            *t1p = aux;
+            if (islower(aux[-1]))
+                aux[-1] = t1[1];
+        } else if (aux[-2] == t2[0] && toupper(aux[-1]) == t2[1]) {
+            *t2p = aux;
+            if (islower(aux[-1]))
+                aux[-1] = t2[1];
+        } else if (aux[-2] == t3[0] && toupper(aux[-1]) == t3[1]) {
+            *t3p = aux;
+            if (islower(aux[-1]))
+                aux[-1] = t3[1];
+        }
+        aux = bam_aux_next(b, aux);
+    }
+}
+
+// Return 5' and 3' CIGAR hard-clip counts
+static inline void hard_clips(bam1_t *b, int *end5, int *end3) {
+    uint32_t *cigar = bam_get_cigar(b);
+    int ncigar = b->core.n_cigar;
+    int endL = 0, endR = 0, nh = 0;
+
+    if (ncigar && bam_cigar_op(cigar[0]) == BAM_CHARD_CLIP)
+        endL = bam_cigar_oplen(cigar[0]), nh=1;
+    if (ncigar > nh && bam_cigar_op(cigar[ncigar-1]) == BAM_CHARD_CLIP)
+        endR = bam_cigar_oplen(cigar[ncigar-1]);
+
+    if (b->core.flag & BAM_FREVERSE) {
+        *end5 = endR;
+        *end3 = endL;
+    } else {
+        *end5 = endL;
+        *end3 = endR;
+    }
+}
+
+// Get MM, ML and MN tags, and 5' and 3' hard-clip lengths.
+// MNi is integer copy of MN, or -1 if absent/invalid
+void get_mod_info(bam1_t *b, uint8_t **MM, uint8_t **ML, uint8_t **MN,
+                  int *MNi, int *end5, int *end3) {
+    find_tags(b, "MM", MM, "ML", ML, "MN", MN);
+    if (*MN) {
+        int save_errno = errno;
+        errno = 0;
+        *MNi = bam_aux2i(*MN);
+        if (errno == EINVAL)
+            *MNi = -1;
+        errno = save_errno;
+    } else {
+        *MNi = -1;
+    }
+
+    if (*MM)
+        hard_clips(b, end5, end3);
+    else
+        *end5 = *end3 = 0; // don't need if MM not found
+}
+
+typedef struct MM_state {
+    // tags found on "pre" BAM
+    uint8_t *MM, *ML, *MN;
+} MM_state;
+
+uint8_t *MN_enc(uint8_t *tag, uint32_t n) {
+    if (n > UINT16_MAX) {
+        tag[0] = 'I';
+        i32_to_le(n, tag+1);
+        tag += 5;
+    } else if (n > UINT8_MAX) {
+        tag[0] = 'S';
+        i16_to_le(n, tag+1);
+        tag += 3;
+    } else {
+        *tag++ = 'C';
+        *tag++ = n;
+    }
+
+    return tag;
+}
+
+// Trim 5'/3' bases off MM and ML tags, using a previous sequence as a guide.
+int trim_MM(bam1_t *pre, bam1_t *cur, int end5, int end3,
+            uint8_t *MM, uint8_t *ML, uint8_t *MN) {
+    // Count number of bases
+    int counts5[16] = {0}, counts3[16] = {0};
+
+    uint8_t *seq = bam_get_seq(pre);
+    int i;
+    for (i = 0; i < end5; i++)
+        counts5[bam_seqi(seq, i)]++;
+    memcpy(counts3, counts5, 16 * sizeof(*counts3));
+    for (; i < pre->core.l_qseq - end3; i++)
+        counts3[bam_seqi(seq, i)]++;
+
+    // "p" is position in pre.
+    // "q" is position in cur.
+    // Hence move up "p" to start and copy from there to "q".
+    uint8_t *MMp, *MLp, *MMq = NULL, *MLq = NULL;
+    if (ML && ML[0] == 'B' && ML[1] == 'C') {
+        MLp = ML+6;
+    } else {
+        ML = MLp = NULL;
+    }
+    MMq = MM+1;
+    MLq = MLp;
+    for (MMp = MM+1; *MMp; ) {
+        int fundamental = seq_nt16_table[*MMp];
+        while (*MMp && *MMp != ',')
+            *MMq++ = *MMp++;
+        if (*MMp)
+            *MMq++ = *MMp++;
+
+        // Now on comma separated list for MM and BC array for ML. Skip
+        int n = 0;
+        while (*MMp != ';' && n < counts5[fundamental]) {
+            char *endptr;
+            long delta = strtol((char *)MMp, &endptr, 10);
+            if (counts5[fundamental] - n > delta) {
+                // Skip entire delta in MM and ML.
+                // Eg counts[]=10, MM=3,10 ML=<10><20> => MM=10 ML=<20>
+                n += delta+1;
+                if(ML) MLp++;
+            } else if (counts3[fundamental] > counts5[fundamental]) {
+                // Shrink delta, writing MM and ML is unchanged.
+                // Eg counts[]=3, MM=10,4 ML=<10><20> => MM=7,4 ML=<10><20>
+                char num[50];
+                int l = sprintf(num, "%ld",
+                                delta - (counts5[fundamental]-n));
+                memcpy((char *)MMq, num, l);
+                MMq += l;
+                *MMq++ = *endptr;
+                n += delta+1;
+                if (ML)
+                    *MLq++ = *MLp++;
+            } else {
+                // next base mod is on boundary of 3' clip point
+                break;
+            }
+
+            MMp = (uint8_t *)endptr;
+            if (*MMp != ',')
+                // error?  if not ; also?
+                break;
+            MMp++;
+        }
+
+        // Copy
+        while (*MMp != ';' && n < counts3[fundamental]) {
+            char *endptr;
+            long delta = strtol((char *)MMp, &endptr, 10);
+            if (counts3[fundamental] - n > delta) {
+                // Copy entire delta in MM and ML including [,;]
+                memmove(MMq, MMp, (uint8_t *)endptr - MMp + 1);
+                MMq += (uint8_t *)endptr - MMp + 1;
+                n += delta+1;
+                if (ML)
+                    *MLq++ = *MLp++;
+            } else {
+                // Next mod is into 3' cutoff, so can terminate MM/ML now
+                n = counts3[fundamental];
+                if (ML)
+                    MLp++;
+            }
+
+            MMp = (uint8_t *)endptr;
+            if (*MMp != ',')
+                break;
+            MMp++;
+        }
+
+        // Skip
+        while (*MMp && *MMp != ';') {
+            while (*MMp && *MMp != ',' && *MMp != ';')
+                MMp++;
+            if (*MMp == ',')
+                MMp++;
+
+            if (ML)
+                MLp++;
+        }
+        MMq[-1] = ';'; // replaces , with ; if clipping right
+        if (*MMp)
+            MMp++;
+    }
+
+    MMp++; // skip nul
+    *MMq++ = 0;
+
+    // Adjust ML B array length
+    if (ML)
+        u32_to_le(MLq-(ML+6), ML+2);
+
+    // Move MM and ML down to include their MM:Z and ML:B bits
+    if (MM) MM-=2;
+    if (ML) ML-=2;
+
+    // Now MM/ML are start of tags, MMq/MLq are ends of edited tags,
+    // and MMp/MLp are ends of original tags.  Walk through tags taking up
+    // any gaps
+    //
+    // Eg XXXXXXmmmmm--YYYlllll-ZZ (m and l are edited MM and ML tags)
+    // => XXXXXXmmmmmYYYlllllZZ
+
+    uint8_t *tag = bam_get_aux(cur), *tag_end = cur->data + cur->l_data;
+    uint8_t *to = tag;
+    while (tag && tag < tag_end) {
+        if (tag[0] == 'M' && (tag[1] == 'M' || tag[1] == 'm')) {
+            // Slow but easy
+            memmove(to, MM, MMq-MM); // length of new tag
+            to += MMq-MM;
+            tag = MMp; // size of old tag
+        } else if (tag[0] == 'M' && (tag[1] == 'L' || tag[1] == 'l')) {
+            memmove(to, ML, MLq-ML);
+            to += MLq-ML;
+            tag = MLp;
+        } else if (tag[0] == 'M' && tag[1] == 'N') {
+            tag = bam_aux_next(cur, tag+2);
+            // Skip it as we'll overwrite this later, although this
+            // does change the tag order.  Instead we could do:
+            //
+            // *to++ = 'M';
+            // *to++ = 'N';
+            // to = MN_enc(to, cur->core.l_qseq);
+        } else {
+            // Want aux_skip, but it's private.
+            // So we use bam_aux_next with work-arounds. :(
+            uint8_t *from = tag;
+            tag = bam_aux_next(cur, tag+2);
+            tag = tag ? tag-2 : tag_end;
+            memmove(to, from, tag-from);
+            to += tag-from;
+        }
+    }
+    cur->l_data = to - cur->data;
+
+    return 0;
+}
+
+// Removes base modification tags: MM, ML and MN.
+// This is more efficient than a series of bam_aux_remove and
+// bam_aux_find calls, as the previous removes shuffle the tags we've
+// previously found.  However it's still not optimal.
+void delete_mod_tags(bam1_t *b) {
+    uint8_t *tag = bam_aux_first(b), *next;
+    uint8_t *to = tag;
+    while (tag) {
+        next = bam_aux_next(b, tag);
+        if (tag[-2] == 'M' &&
+            (tag[-1] == 'M' || tag[-1] == 'm' ||
+             tag[-1] == 'L' || tag[-1] == 'l' ||
+             tag[-1] == 'N')) {
+            // Skip. Equivalent to bam_aux_remove without multiple passes
+        } else {
+            // Copy.  All these +/-2s are an annoyance caused by the
+            // tag iterator pointing to the byte after the 2-letter code
+            uint8_t *end = next ? next : b->data + b->l_data + 2;
+            if (tag != to)
+                memmove(to-2, tag-2, end-tag);
+            to += end-tag;
+        }
+        tag = next;
+    }
+
+    b->l_data = (to-2) - b->data;
+}
+
+int validate_MM(bam1_t *b, hts_base_mod_state *state) {
+    hts_base_mod mods[10];
+    int n, pos;
+    while ((n = bam_next_basemod(b, state, mods, 10, &pos)) > 0) {
+        // bam_next_basemod will trigger MM out-of-bound checks
+    }
+    return n;
+}
+
+// Fix base modification tags MM, ML and MN.
+// For supplementary-style alignments we may have hard-clipped the sequence
+// and just duplicated the MM/ML tags.  Use the primary alignment to get the
+// clipped sequence so we can trim MM/ML accordingly.
+//
+// We call this first on primary reads with pre == NULL.  This caches
+// MM and ML data into MM_state.
+//
+// We then call it again on secondary and/or supplementary data with
+// pre == the primary record and pass in the associated state.  This then
+// validates MM/ML/MN match, and if not adjusts them if they have hard-clips
+// which yields consistent data.
+//
+// TODO: add sanity check on counts of base types and MM tag to ensure it's
+// possible. We can do this post-trimming, so we sanitize everything.
+//
+// Returns 0 on success,
+//        -1 on failure
+int fix_MM(bam1_t *pre, bam1_t *cur, MM_state *state) {
+    int end5, end3;
+    int MNi = 0; // MN of -1 is used as indicator for no valid mods
+
+    if (!pre && state) {
+        // First time we've see this name.
+        // Look for base modification tags and sanity check.
+        get_mod_info(cur, &state->MM, &state->ML, &state->MN, &MNi,
+                     &end5, &end3);
+        if (!state->MM) {
+            delete_mod_tags(cur);
+            return 0;
+        }
+
+        if (!end5 && !end3 && MNi <= 0) {
+            // No MN tag, but also no clipping.  Assume MM is valid
+            if (cur->core.l_qseq)
+                if (bam_aux_update_int(cur, "MN", cur->core.l_qseq) < 0)
+                    return -1;
+        } else if ((end3 || end5) && cur->core.l_qseq != MNi) {
+            // We have hard clips and MN tag, but the MN tag doesn't match
+            // observed sequence length so it appears the hard-clipping
+            // happened after base-mods called without updating.
+            // Fail as this is a primary read.
+            delete_mod_tags(cur);
+        }
+        // Otherwise we assume the base modifications are correct
+
+    } else if (state) {
+        // A supplementary or secondary alignment with known primary
+        uint8_t *cur_MM = NULL, *cur_ML = NULL, *cur_MN = NULL;
+        MNi = -1;
+        get_mod_info(cur, &cur_MM, &cur_ML, &cur_MN, &MNi, &end5, &end3);
+
+        if (!cur_MM) {
+            delete_mod_tags(cur);
+            return 0;
+        }
+
+        // Does MN match seq length?  If so, we believe it's already valid
+        if (MNi == cur->core.l_qseq)
+            goto validate;
+
+        // Length mismatch and/or no known length, so check vs full seq.
+        if (pre->core.l_qseq != cur->core.l_qseq + end3 + end5) {
+            delete_mod_tags(cur);
+            return 0;
+        } else if (end5 || end3) {
+             if (MNi < 0 || MNi == pre->core.l_qseq)
+                 trim_MM(pre, cur, end5, end3, cur_MM, cur_ML, cur_MN);
+        } // else no hard clips so MM is already valid
+
+        // Set MN so we've validated it, provided seq isn't "*".
+        // inefficient, but minimal compared to everything else
+        if (cur->core.l_qseq)
+            if (bam_aux_update_int(cur, "MN", cur->core.l_qseq) < 0)
+                return -1;
+    }
+
+ validate:
+    ;
+
+    // Also validate MM length matches sequence length.  This mirrors the
+    // logic in htslib/sam_mods.c.
+    // For now we take the inefficient approach of using bam_parse_basemod2.
+    // Inefficient, but robust.
+    hts_base_mod_state *mst = hts_base_mod_state_alloc();
+    if (!mst)
+        return -1;
+
+    enum htsLogLevel lvl = hts_get_log_level();
+    hts_set_log_level(HTS_LOG_OFF);
+    if (bam_parse_basemod(cur, mst) < 0)
+        // Maybe we want hts_log_warning still though?
+        delete_mod_tags(cur);
+    if (validate_MM(cur, mst) < 0)
+        delete_mod_tags(cur);
+    hts_set_log_level(lvl);
+    hts_base_mod_state_free(mst);
+
+    return 0;
+}
+
+// Ensure the b[] array is at least n.
+// Returns 0 on success,
+//        -1 on failure
+static int grow_b_array(bam1_t **b, int *ba, int n) {
+    if (n < *ba)
+        return 0;
+
+    bam1_t *bnew = realloc(*b, (n+=10) * sizeof(**b));
+    if (!bnew)
+        return -1;
+    *b = bnew;
+
+    // bam_init1 equivalent
+    int i;
+    for (i = *ba; i < n; i++)
+        memset(&(*b)[i], 0, sizeof(bam1_t));
+
+    *b = bnew;
+    *ba = n;
+
+    return 0;
+}
+
+// We have b[0]..b[bn-1] entries all from the same template (qname)
+typedef struct {
+    bam1_t *b;
+    int n, ba;  // number used and number allocated
+    int b_next; // b[b_next] for start of next set, -1 if unset
+    int eof;    // marker for having seen eof
+} bam_set;
+
+// Fetches a new batch of BAM records all containing the same name.
+// NB: we cache the last (non-matching) name in b[n], so we can use it to
+// start the next batch.
+// Returns the number of records on success,
+//         <0 on failure or EOF (sam_read1 return vals)
+static int next_template(samFile *in, sam_hdr_t *header, bam_set *bs,
+                         int sanitize_flags) {
+    int result;
+
+    if (bs->eof)
+        return -1;
+
+    // First time through, prime the template name
+    if (bs->b_next < 0) {
+        if (grow_b_array(&bs->b, &bs->ba, 1) < 0)
+            return -2;
+        result = sam_read1(in, header, &bs->b[0]);
+        if (result < 0)
+            return result;
+        if (bam_sanitize(header, &bs->b[0], sanitize_flags) < 0)
+            return -2;
+    } else {
+        // Otherwise use the previous template name read
+        bam1_t btmp = bs->b[0];
+        bs->b[0] = bs->b[bs->b_next];
+        bs->b[bs->b_next] = btmp; // For ->{,l_,m_}data
+    }
+    bs->n = 1;
+
+    // Now keep reading until we find a read that mismatches or we hit eof.
+    char *name = bam_get_qname(&bs->b[0]);
+    for (;;) {
+        if (grow_b_array(&bs->b, &bs->ba, bs->n+1) < 0)
+            return -2;
+
+        result = sam_read1(in, header, &bs->b[bs->n]);
+        if (result < -1)
+            return result;
+
+        if (result < 0) {
+            bs->eof = 1;
+            bs->b_next = -1;
+            break;
+        } else {
+            if (bam_sanitize(header, &bs->b[bs->n], sanitize_flags) < 0)
+                return -2;
+
+            bs->b_next = bs->n;
+            if (strcmp(name, bam_get_qname(&bs->b[bs->n])) != 0)
+                break;
+        }
+
+        bs->n++;
+    }
+
+    return bs->n;
+}
+
 // currently, this function ONLY works if each read has one hit
+//
+// Returns 0 on success,
+//        >0 on failure
 static int bam_mating_core(samFile *in, samFile *out, int remove_reads,
                            int proper_pair_check, int add_ct,
                            int do_mate_scoring, char *arg_list, int no_pg,
-                           int sanitize_flags)
+                           int sanitize_flags, int base_mods)
 {
     sam_hdr_t *header;
-    bam1_t *b[2] = { NULL, NULL };
-    int curr, has_prev, result;
-    hts_pos_t pre_end = 0, cur_end = 0;
+    int result, n;
     kstring_t str = KS_INITIALIZE;
+    bam_set bs = {NULL, 0, 0, -1, 0};
 
     header = sam_hdr_read(in);
     if (header == NULL) {
@@ -506,101 +985,140 @@ static int bam_mating_core(samFile *in, samFile *out, int remove_reads,
 
     if (sam_hdr_write(out, header) < 0) goto write_fail;
 
-    b[0] = bam_init1();
-    b[1] = bam_init1();
-    curr = 0; has_prev = 0;
-    while ((result = sam_read1(in, header, b[curr])) >= 0) {
-        bam1_t *cur = b[curr], *pre = b[1-curr];
-        if (bam_sanitize(header, cur, sanitize_flags) < 0)
-            goto fail;
-        if (cur->core.flag & BAM_FSECONDARY)
-        {
-            if ( !remove_reads ) {
-                if (sam_write1(out, header, cur) < 0) goto write_fail;
+    // Iterate template by template fetching bs->n records at a time
+    while ((result = next_template(in, header, &bs, sanitize_flags)) >= 0) {
+        bam1_t *cur = NULL, *pre = NULL, *rnum[2] = {NULL, NULL};
+        int prev = -1, curr = -1;
+        hts_pos_t pre_end = 0, cur_end = 0;
+
+        // Find and fix up primary alignments
+        MM_state state[2];
+        for (n = 0; n < bs.n; n++) {
+            int is_r2 = (bs.b[n].core.flag & BAM_FREAD2) != 0;
+            if (bs.b[n].core.flag & (BAM_FSECONDARY | BAM_FSUPPLEMENTARY))
+                continue;
+
+            if (base_mods)
+                if (fix_MM(NULL, &bs.b[n], &state[is_r2]) < 0)
+                    goto fail;
+
+            if (!pre) {
+                pre = &bs.b[prev = n];
+                rnum[(pre->core.flag & BAM_FREAD2) != 0] = pre;
+
+                pre_end = (pre->core.flag & BAM_FUNMAP) == 0
+                    ? bam_endpos(pre) : 0;
+                continue;
             }
-            continue; // skip secondary alignments
-        }
-        if (cur->core.flag & BAM_FSUPPLEMENTARY)
-        {
-            if (sam_write1(out, header, cur) < 0) goto write_fail;
-            continue; // pass supplementary alignments through unchanged (TODO:make them match read they came from)
-        }
-        if ((cur->core.flag&BAM_FUNMAP) == 0) // If mapped calculate end
-        {
-            cur_end = bam_endpos(cur);
-        }
 
-        if (has_prev) { // do we have a pair of reads to examine?
-            if (strcmp(bam_get_qname(cur), bam_get_qname(pre)) == 0) { // identical pair name
-                pre->core.flag |= BAM_FPAIRED;
-                cur->core.flag |= BAM_FPAIRED;
-                if (sync_mate(pre, cur)) goto fail;
-
-                if (pre->core.tid == cur->core.tid && !(cur->core.flag&(BAM_FUNMAP|BAM_FMUNMAP))
-                    && !(pre->core.flag&(BAM_FUNMAP|BAM_FMUNMAP))) // if safe set TLEN/ISIZE
-                {
-                    hts_pos_t cur5, pre5;
-                    cur5 = (cur->core.flag&BAM_FREVERSE)? cur_end : cur->core.pos;
-                    pre5 = (pre->core.flag&BAM_FREVERSE)? pre_end : pre->core.pos;
-                    cur->core.isize = pre5 - cur5; pre->core.isize = cur5 - pre5;
-                } else cur->core.isize = pre->core.isize = 0;
-                if (add_ct) bam_template_cigar(pre, cur, &str);
-                // TODO: Add code to properly check if read is in a proper pair based on ISIZE distribution
-                if (proper_pair_check && !plausibly_properly_paired(pre,cur)) {
-                    pre->core.flag &= ~BAM_FPROPER_PAIR;
-                    cur->core.flag &= ~BAM_FPROPER_PAIR;
-                }
+            // Note, more than 2 primary alignments will use 'curr' as last
+            cur = &bs.b[curr = n];
+            rnum[(cur->core.flag & BAM_FREAD2) != 0] = cur;
+            cur_end = (cur->core.flag & BAM_FUNMAP) == 0
+                ? bam_endpos(cur) : 0;
+
+            pre->core.flag |= BAM_FPAIRED;
+            cur->core.flag |= BAM_FPAIRED;
+            if (sync_mate(pre, cur))
+                goto fail;
+
+            // If safe set TLEN/ISIZE
+            if (pre->core.tid == cur->core.tid
+                && !(cur->core.flag & (BAM_FUNMAP | BAM_FMUNMAP))
+                && !(pre->core.flag & (BAM_FUNMAP | BAM_FMUNMAP))) {
+                hts_pos_t cur5, pre5;
+                cur5 = (cur->core.flag & BAM_FREVERSE)
+                    ? cur_end
+                    : cur->core.pos;
+                pre5 = (pre->core.flag & BAM_FREVERSE)
+                    ? pre_end
+                    : pre->core.pos;
+                cur->core.isize = pre5 - cur5;
+                pre->core.isize = cur5 - pre5;
+            } else {
+                cur->core.isize = pre->core.isize = 0;
+            }
 
-                if (do_mate_scoring) {
-                    if ((add_mate_score(pre, cur) == -1) || (add_mate_score(cur, pre) == -1)) {
-                        fprintf(stderr, "[bam_mating_core] ERROR: unable to add mate score.\n");
-                        goto fail;
-                    }
-                }
+            if (add_ct)
+                bam_template_cigar(pre, cur, &str);
 
-                // Write out result
-                if ( !remove_reads ) {
-                    if (sam_write1(out, header, pre) < 0) goto write_fail;
-                    if (sam_write1(out, header, cur) < 0) goto write_fail;
-                } else {
-                    // If we have to remove reads make sure we do it in a way that doesn't create orphans with bad flags
-                    if(pre->core.flag&BAM_FUNMAP) cur->core.flag &= ~(BAM_FPAIRED|BAM_FMREVERSE|BAM_FPROPER_PAIR);
-                    if(cur->core.flag&BAM_FUNMAP) pre->core.flag &= ~(BAM_FPAIRED|BAM_FMREVERSE|BAM_FPROPER_PAIR);
-                    if(!(pre->core.flag&BAM_FUNMAP)) {
-                        if (sam_write1(out, header, pre) < 0) goto write_fail;
-                    }
-                    if(!(cur->core.flag&BAM_FUNMAP)) {
-                        if (sam_write1(out, header, cur) < 0) goto write_fail;
-                    }
-                }
-                has_prev = 0;
-            } else { // unpaired?  clear bad info and write it out
-                pre->core.mtid = -1; pre->core.mpos = -1; pre->core.isize = 0;
-                pre->core.flag &= ~(BAM_FPAIRED|BAM_FMREVERSE|BAM_FPROPER_PAIR);
-                if ( !remove_reads || !(pre->core.flag&BAM_FUNMAP) ) {
-                    if (sam_write1(out, header, pre) < 0) goto write_fail;
+            // TODO: Add code to properly check if read is in a proper
+            // pair based on ISIZE distribution
+            if (proper_pair_check && !plausibly_properly_paired(pre,cur)) {
+                pre->core.flag &= ~BAM_FPROPER_PAIR;
+                cur->core.flag &= ~BAM_FPROPER_PAIR;
+            }
+
+            if (do_mate_scoring) {
+                if ((add_mate_score(pre, cur) == -1) ||
+                    (add_mate_score(cur, pre) == -1)) {
+                    fprintf(stderr, "[bam_mating_core] ERROR: "
+                            "unable to add mate score.\n");
+                    goto fail;
                 }
             }
-        } else has_prev = 1;
-        curr = 1 - curr;
-        pre_end = cur_end;
-    }
-    if (result < -1) goto read_fail;
-    if (has_prev && !remove_reads) { // If we still have a BAM in the buffer it must be unpaired
-        bam1_t *pre = b[1-curr];
-        if (pre->core.tid < 0 || pre->core.pos < 0 || pre->core.flag&BAM_FUNMAP) { // If unmapped
-            pre->core.flag |= BAM_FUNMAP;
-            pre->core.tid = -1;
-            pre->core.pos = -1;
+
+            // If we have to remove reads make sure we do it in a way that
+            // doesn't create orphans with bad flags
+            if (remove_reads) {
+                if (pre->core.flag&BAM_FUNMAP)
+                    cur->core.flag &=
+                        ~(BAM_FMREVERSE|BAM_FPROPER_PAIR);
+                if (cur->core.flag&BAM_FUNMAP)
+                    pre->core.flag &=
+                        ~(BAM_FMREVERSE|BAM_FPROPER_PAIR);
+            }
         }
-        pre->core.mtid = -1; pre->core.mpos = -1; pre->core.isize = 0;
-        pre->core.flag &= ~(BAM_FPAIRED|BAM_FMREVERSE|BAM_FPROPER_PAIR);
 
-        if (sam_write1(out, header, pre) < 0) goto write_fail;
+        // Handle unpaired primary data
+        if (!cur && pre) {
+            pre->core.mtid = -1;
+            pre->core.mpos = -1;
+            pre->core.isize = 0;
+            pre->core.flag &= ~(BAM_FMREVERSE|BAM_FPROPER_PAIR);
+        }
+
+        // Now process secondary and supplementary alignments
+        for (n = 0; n < bs.n; n++) {
+            if (!(bs.b[n].core.flag & (BAM_FSECONDARY|BAM_FSUPPLEMENTARY))) {
+                // primary
+                continue;
+            }
+
+            // Secondary or supplementary
+            int is_r2 = (bs.b[n].core.flag & BAM_FREAD2) != 0;
+            bam1_t *primary = rnum[is_r2];
+            if (primary) {
+                if (base_mods)
+                    fix_MM(primary, &bs.b[n], &state[is_r2]);
+            } else {
+                // Record with base modifications but no known primary
+                //fprintf(stderr, "Unpaired secondary or supplementary\n");
+                if (base_mods)
+                    fix_MM(NULL, &bs.b[n], NULL);
+            }
+        }
+
+        // Finally having curated everything, write out all records in their
+        // original ordering
+        for (n = 0; n < bs.n; n++) {
+            bam1_t *cur = &bs.b[n];
+            // We may remove unmapped and secondary alignments
+            if (remove_reads && (cur->core.flag & (BAM_FSECONDARY|BAM_FUNMAP)))
+                continue;
+
+            if (sam_write1(out, header, cur) < 0)
+                goto write_fail;
+        }
     }
+    if (result < -1)
+        goto read_fail;
+
     sam_hdr_destroy(header);
-    bam_destroy1(b[0]);
-    bam_destroy1(b[1]);
+
+    for (n = 0; n < bs.ba; n++)
+        free(bs.b[n].data);
+    free(bs.b);
     ks_free(&str);
     return 0;
 
@@ -612,8 +1130,9 @@ static int bam_mating_core(samFile *in, samFile *out, int remove_reads,
     print_error_errno("fixmate", "Couldn't write to output file");
  fail:
     sam_hdr_destroy(header);
-    bam_destroy1(b[0]);
-    bam_destroy1(b[1]);
+    for (n = 0; n < bs.ba; n++)
+        free(bs.b[n].data);
+    free(bs.b);
     ks_free(&str);
     return 1;
 }
@@ -630,6 +1149,7 @@ void usage(FILE* where)
 "  -u           Uncompressed output\n"
 "  -z, --sanitize FLAG[,FLAG]\n"
 "               Sanitize alignment fields [defaults to all types]\n"
+"  -M           Fix base modification tags (MM/ML/MN)\n"
 "  --no-PG      do not add a PG line\n");
 
     sam_global_opt_help(where, "-.O..@-.");
@@ -646,7 +1166,7 @@ int bam_mating(int argc, char *argv[])
     htsThreadPool p = {NULL, 0};
     samFile *in = NULL, *out = NULL;
     int c, remove_reads = 0, proper_pair_check = 1, add_ct = 0, res = 1,
-        mate_score = 0, no_pg = 0, sanitize_flags = FIX_ALL;
+        mate_score = 0, no_pg = 0, sanitize_flags = FIX_ALL, base_mods = 0;
     sam_global_args ga = SAM_GLOBAL_ARGS_INIT;
     char wmode[4] = {'w', 'b', 0, 0};
     static const struct option lopts[] = {
@@ -658,12 +1178,13 @@ int bam_mating(int argc, char *argv[])
 
     // parse args
     if (argc == 1) { usage(stdout); return 0; }
-    while ((c = getopt_long(argc, argv, "rpcmO:@:uz:", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "rpcmMO:@:uz:", lopts, NULL)) >= 0) {
         switch (c) {
         case 'r': remove_reads = 1; break;
         case 'p': proper_pair_check = 0; break;
         case 'c': add_ct = 1; break;
         case 'm': mate_score = 1; break;
+        case 'M': base_mods = 1; break;
         case 'u': wmode[2] = '0'; break;
         case 1: no_pg = 1; break;
         default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
@@ -702,7 +1223,8 @@ int bam_mating(int argc, char *argv[])
 
     // run
     res = bam_mating_core(in, out, remove_reads, proper_pair_check, add_ct,
-                          mate_score, arg_list, no_pg, sanitize_flags);
+                          mate_score, arg_list, no_pg, sanitize_flags,
+                          base_mods);
 
     // cleanup
     sam_close(in);
index 1796f6e9fb2e3e374787ae33136ea8a357c37917..a0ba82051e56fedc626d27e1c8af0f9e6c921fc8 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  bam_mate.c -- fix mate pairing information and clean up flags.
 
-    Copyright (C) 2009, 2011-2017, 2019, 2022 Genome Research Ltd.
+    Copyright (C) 2009, 2011-2017, 2019, 2022, 2024 Genome Research Ltd.
     Portions copyright (C) 2011 Broad Institute.
     Portions copyright (C) 2012 Peter Cock, The James Hutton Institute.
 
@@ -33,6 +33,7 @@ DEALINGS IN THE SOFTWARE.  */
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
+#include <ctype.h>
 #include "htslib/thread_pool.h"
 #include "sam_opts.h"
 #include "htslib/kstring.h"
@@ -92,7 +93,7 @@ static void bam_template_cigar(bam1_t *b1, bam1_t *b2, kstring_t *str)
  * single Reads:
  * -if pos == 0 (1 based), tid == -1, or UNMAPPED then set UNMAPPED, pos = 0,
  *  tid = -1
- * -clear bad flags (PAIRED, MREVERSE, PROPER_PAIR)
+ * -clear bad flags (MREVERSE, PROPER_PAIR)
  * -set mpos = 0 (1 based), mtid = -1 and isize = 0
  * -write to output
  * Paired Reads:
@@ -474,17 +475,495 @@ int bam_sanitize(sam_hdr_t *h, bam1_t *b, int flags) {
     return 0;
 }
 
+// Look for 3 tags in one pass, for efficiencies sake.
+// We also convert the draft tags Mm and Ml to MM and ML here.
+static inline void find_tags(bam1_t *b,
+                             char *t1, uint8_t **t1p,
+                             char *t2, uint8_t **t2p,
+                             char *t3, uint8_t **t3p) {
+    *t1p = *t2p = *t3p = NULL;
+    uint8_t *aux = bam_aux_first(b);
+
+    while (aux) {
+        if (aux[-2] == t1[0] && toupper(aux[-1]) == t1[1]) {
+            *t1p = aux;
+            if (islower(aux[-1]))
+                aux[-1] = t1[1];
+        } else if (aux[-2] == t2[0] && toupper(aux[-1]) == t2[1]) {
+            *t2p = aux;
+            if (islower(aux[-1]))
+                aux[-1] = t2[1];
+        } else if (aux[-2] == t3[0] && toupper(aux[-1]) == t3[1]) {
+            *t3p = aux;
+            if (islower(aux[-1]))
+                aux[-1] = t3[1];
+        }
+        aux = bam_aux_next(b, aux);
+    }
+}
+
+// Return 5' and 3' CIGAR hard-clip counts
+static inline void hard_clips(bam1_t *b, int *end5, int *end3) {
+    uint32_t *cigar = bam_get_cigar(b);
+    int ncigar = b->core.n_cigar;
+    int endL = 0, endR = 0, nh = 0;
+
+    if (ncigar && bam_cigar_op(cigar[0]) == BAM_CHARD_CLIP)
+        endL = bam_cigar_oplen(cigar[0]), nh=1;
+    if (ncigar > nh && bam_cigar_op(cigar[ncigar-1]) == BAM_CHARD_CLIP)
+        endR = bam_cigar_oplen(cigar[ncigar-1]);
+
+    if (b->core.flag & BAM_FREVERSE) {
+        *end5 = endR;
+        *end3 = endL;
+    } else {
+        *end5 = endL;
+        *end3 = endR;
+    }
+}
+
+// Get MM, ML and MN tags, and 5' and 3' hard-clip lengths.
+// MNi is integer copy of MN, or -1 if absent/invalid
+void get_mod_info(bam1_t *b, uint8_t **MM, uint8_t **ML, uint8_t **MN,
+                  int *MNi, int *end5, int *end3) {
+    find_tags(b, "MM", MM, "ML", ML, "MN", MN);
+    if (*MN) {
+        int save_errno = errno;
+        errno = 0;
+        *MNi = bam_aux2i(*MN);
+        if (errno == EINVAL)
+            *MNi = -1;
+        errno = save_errno;
+    } else {
+        *MNi = -1;
+    }
+
+    if (*MM)
+        hard_clips(b, end5, end3);
+    else
+        *end5 = *end3 = 0; // don't need if MM not found
+}
+
+typedef struct MM_state {
+    // tags found on "pre" BAM
+    uint8_t *MM, *ML, *MN;
+} MM_state;
+
+uint8_t *MN_enc(uint8_t *tag, uint32_t n) {
+    if (n > UINT16_MAX) {
+        tag[0] = 'I';
+        i32_to_le(n, tag+1);
+        tag += 5;
+    } else if (n > UINT8_MAX) {
+        tag[0] = 'S';
+        i16_to_le(n, tag+1);
+        tag += 3;
+    } else {
+        *tag++ = 'C';
+        *tag++ = n;
+    }
+
+    return tag;
+}
+
+// Trim 5'/3' bases off MM and ML tags, using a previous sequence as a guide.
+int trim_MM(bam1_t *pre, bam1_t *cur, int end5, int end3,
+            uint8_t *MM, uint8_t *ML, uint8_t *MN) {
+    // Count number of bases
+    int counts5[16] = {0}, counts3[16] = {0};
+
+    uint8_t *seq = bam_get_seq(pre);
+    int i;
+    for (i = 0; i < end5; i++)
+        counts5[bam_seqi(seq, i)]++;
+    memcpy(counts3, counts5, 16 * sizeof(*counts3));
+    for (; i < pre->core.l_qseq - end3; i++)
+        counts3[bam_seqi(seq, i)]++;
+
+    // "p" is position in pre.
+    // "q" is position in cur.
+    // Hence move up "p" to start and copy from there to "q".
+    uint8_t *MMp, *MLp, *MMq = NULL, *MLq = NULL;
+    if (ML && ML[0] == 'B' && ML[1] == 'C') {
+        MLp = ML+6;
+    } else {
+        ML = MLp = NULL;
+    }
+    MMq = MM+1;
+    MLq = MLp;
+    for (MMp = MM+1; *MMp; ) {
+        int fundamental = seq_nt16_table[*MMp];
+        while (*MMp && *MMp != ',')
+            *MMq++ = *MMp++;
+        if (*MMp)
+            *MMq++ = *MMp++;
+
+        // Now on comma separated list for MM and BC array for ML. Skip
+        int n = 0;
+        while (*MMp != ';' && n < counts5[fundamental]) {
+            char *endptr;
+            long delta = strtol((char *)MMp, &endptr, 10);
+            if (counts5[fundamental] - n > delta) {
+                // Skip entire delta in MM and ML.
+                // Eg counts[]=10, MM=3,10 ML=<10><20> => MM=10 ML=<20>
+                n += delta+1;
+                if(ML) MLp++;
+            } else if (counts3[fundamental] > counts5[fundamental]) {
+                // Shrink delta, writing MM and ML is unchanged.
+                // Eg counts[]=3, MM=10,4 ML=<10><20> => MM=7,4 ML=<10><20>
+                char num[50];
+                int l = sprintf(num, "%ld",
+                                delta - (counts5[fundamental]-n));
+                memcpy((char *)MMq, num, l);
+                MMq += l;
+                *MMq++ = *endptr;
+                n += delta+1;
+                if (ML)
+                    *MLq++ = *MLp++;
+            } else {
+                // next base mod is on boundary of 3' clip point
+                break;
+            }
+
+            MMp = (uint8_t *)endptr;
+            if (*MMp != ',')
+                // error?  if not ; also?
+                break;
+            MMp++;
+        }
+
+        // Copy
+        while (*MMp != ';' && n < counts3[fundamental]) {
+            char *endptr;
+            long delta = strtol((char *)MMp, &endptr, 10);
+            if (counts3[fundamental] - n > delta) {
+                // Copy entire delta in MM and ML including [,;]
+                memmove(MMq, MMp, (uint8_t *)endptr - MMp + 1);
+                MMq += (uint8_t *)endptr - MMp + 1;
+                n += delta+1;
+                if (ML)
+                    *MLq++ = *MLp++;
+            } else {
+                // Next mod is into 3' cutoff, so can terminate MM/ML now
+                n = counts3[fundamental];
+                if (ML)
+                    MLp++;
+            }
+
+            MMp = (uint8_t *)endptr;
+            if (*MMp != ',')
+                break;
+            MMp++;
+        }
+
+        // Skip
+        while (*MMp && *MMp != ';') {
+            while (*MMp && *MMp != ',' && *MMp != ';')
+                MMp++;
+            if (*MMp == ',')
+                MMp++;
+
+            if (ML)
+                MLp++;
+        }
+        MMq[-1] = ';'; // replaces , with ; if clipping right
+        if (*MMp)
+            MMp++;
+    }
+
+    MMp++; // skip nul
+    *MMq++ = 0;
+
+    // Adjust ML B array length
+    if (ML)
+        u32_to_le(MLq-(ML+6), ML+2);
+
+    // Move MM and ML down to include their MM:Z and ML:B bits
+    if (MM) MM-=2;
+    if (ML) ML-=2;
+
+    // Now MM/ML are start of tags, MMq/MLq are ends of edited tags,
+    // and MMp/MLp are ends of original tags.  Walk through tags taking up
+    // any gaps
+    //
+    // Eg XXXXXXmmmmm--YYYlllll-ZZ (m and l are edited MM and ML tags)
+    // => XXXXXXmmmmmYYYlllllZZ
+
+    uint8_t *tag = bam_get_aux(cur), *tag_end = cur->data + cur->l_data;
+    uint8_t *to = tag;
+    while (tag && tag < tag_end) {
+        if (tag[0] == 'M' && (tag[1] == 'M' || tag[1] == 'm')) {
+            // Slow but easy
+            memmove(to, MM, MMq-MM); // length of new tag
+            to += MMq-MM;
+            tag = MMp; // size of old tag
+        } else if (tag[0] == 'M' && (tag[1] == 'L' || tag[1] == 'l')) {
+            memmove(to, ML, MLq-ML);
+            to += MLq-ML;
+            tag = MLp;
+        } else if (tag[0] == 'M' && tag[1] == 'N') {
+            tag = bam_aux_next(cur, tag+2);
+            // Skip it as we'll overwrite this later, although this
+            // does change the tag order.  Instead we could do:
+            //
+            // *to++ = 'M';
+            // *to++ = 'N';
+            // to = MN_enc(to, cur->core.l_qseq);
+        } else {
+            // Want aux_skip, but it's private.
+            // So we use bam_aux_next with work-arounds. :(
+            uint8_t *from = tag;
+            tag = bam_aux_next(cur, tag+2);
+            tag = tag ? tag-2 : tag_end;
+            memmove(to, from, tag-from);
+            to += tag-from;
+        }
+    }
+    cur->l_data = to - cur->data;
+
+    return 0;
+}
+
+// Removes base modification tags: MM, ML and MN.
+// This is more efficient than a series of bam_aux_remove and
+// bam_aux_find calls, as the previous removes shuffle the tags we've
+// previously found.  However it's still not optimal.
+void delete_mod_tags(bam1_t *b) {
+    uint8_t *tag = bam_aux_first(b), *next;
+    uint8_t *to = tag;
+    while (tag) {
+        next = bam_aux_next(b, tag);
+        if (tag[-2] == 'M' &&
+            (tag[-1] == 'M' || tag[-1] == 'm' ||
+             tag[-1] == 'L' || tag[-1] == 'l' ||
+             tag[-1] == 'N')) {
+            // Skip. Equivalent to bam_aux_remove without multiple passes
+        } else {
+            // Copy.  All these +/-2s are an annoyance caused by the
+            // tag iterator pointing to the byte after the 2-letter code
+            uint8_t *end = next ? next : b->data + b->l_data + 2;
+            if (tag != to)
+                memmove(to-2, tag-2, end-tag);
+            to += end-tag;
+        }
+        tag = next;
+    }
+
+    b->l_data = (to-2) - b->data;
+}
+
+int validate_MM(bam1_t *b, hts_base_mod_state *state) {
+    hts_base_mod mods[10];
+    int n, pos;
+    while ((n = bam_next_basemod(b, state, mods, 10, &pos)) > 0) {
+        // bam_next_basemod will trigger MM out-of-bound checks
+    }
+    return n;
+}
+
+// Fix base modification tags MM, ML and MN.
+// For supplementary-style alignments we may have hard-clipped the sequence
+// and just duplicated the MM/ML tags.  Use the primary alignment to get the
+// clipped sequence so we can trim MM/ML accordingly.
+//
+// We call this first on primary reads with pre == NULL.  This caches
+// MM and ML data into MM_state.
+//
+// We then call it again on secondary and/or supplementary data with
+// pre == the primary record and pass in the associated state.  This then
+// validates MM/ML/MN match, and if not adjusts them if they have hard-clips
+// which yields consistent data.
+//
+// TODO: add sanity check on counts of base types and MM tag to ensure it's
+// possible. We can do this post-trimming, so we sanitize everything.
+//
+// Returns 0 on success,
+//        -1 on failure
+int fix_MM(bam1_t *pre, bam1_t *cur, MM_state *state) {
+    int end5, end3;
+    int MNi = 0; // MN of -1 is used as indicator for no valid mods
+
+    if (!pre && state) {
+        // First time we've see this name.
+        // Look for base modification tags and sanity check.
+        get_mod_info(cur, &state->MM, &state->ML, &state->MN, &MNi,
+                     &end5, &end3);
+        if (!state->MM) {
+            delete_mod_tags(cur);
+            return 0;
+        }
+
+        if (!end5 && !end3 && MNi <= 0) {
+            // No MN tag, but also no clipping.  Assume MM is valid
+            if (cur->core.l_qseq)
+                if (bam_aux_update_int(cur, "MN", cur->core.l_qseq) < 0)
+                    return -1;
+        } else if ((end3 || end5) && cur->core.l_qseq != MNi) {
+            // We have hard clips and MN tag, but the MN tag doesn't match
+            // observed sequence length so it appears the hard-clipping
+            // happened after base-mods called without updating.
+            // Fail as this is a primary read.
+            delete_mod_tags(cur);
+        }
+        // Otherwise we assume the base modifications are correct
+
+    } else if (state) {
+        // A supplementary or secondary alignment with known primary
+        uint8_t *cur_MM = NULL, *cur_ML = NULL, *cur_MN = NULL;
+        MNi = -1;
+        get_mod_info(cur, &cur_MM, &cur_ML, &cur_MN, &MNi, &end5, &end3);
+
+        if (!cur_MM) {
+            delete_mod_tags(cur);
+            return 0;
+        }
+
+        // Does MN match seq length?  If so, we believe it's already valid
+        if (MNi == cur->core.l_qseq)
+            goto validate;
+
+        // Length mismatch and/or no known length, so check vs full seq.
+        if (pre->core.l_qseq != cur->core.l_qseq + end3 + end5) {
+            delete_mod_tags(cur);
+            return 0;
+        } else if (end5 || end3) {
+             if (MNi < 0 || MNi == pre->core.l_qseq)
+                 trim_MM(pre, cur, end5, end3, cur_MM, cur_ML, cur_MN);
+        } // else no hard clips so MM is already valid
+
+        // Set MN so we've validated it, provided seq isn't "*".
+        // inefficient, but minimal compared to everything else
+        if (cur->core.l_qseq)
+            if (bam_aux_update_int(cur, "MN", cur->core.l_qseq) < 0)
+                return -1;
+    }
+
+ validate:
+    ;
+
+    // Also validate MM length matches sequence length.  This mirrors the
+    // logic in htslib/sam_mods.c.
+    // For now we take the inefficient approach of using bam_parse_basemod2.
+    // Inefficient, but robust.
+    hts_base_mod_state *mst = hts_base_mod_state_alloc();
+    if (!mst)
+        return -1;
+
+    enum htsLogLevel lvl = hts_get_log_level();
+    hts_set_log_level(HTS_LOG_OFF);
+    if (bam_parse_basemod(cur, mst) < 0)
+        // Maybe we want hts_log_warning still though?
+        delete_mod_tags(cur);
+    if (validate_MM(cur, mst) < 0)
+        delete_mod_tags(cur);
+    hts_set_log_level(lvl);
+    hts_base_mod_state_free(mst);
+
+    return 0;
+}
+
+// Ensure the b[] array is at least n.
+// Returns 0 on success,
+//        -1 on failure
+static int grow_b_array(bam1_t **b, int *ba, int n) {
+    if (n < *ba)
+        return 0;
+
+    bam1_t *bnew = realloc(*b, (n+=10) * sizeof(**b));
+    if (!bnew)
+        return -1;
+    *b = bnew;
+
+    // bam_init1 equivalent
+    int i;
+    for (i = *ba; i < n; i++)
+        memset(&(*b)[i], 0, sizeof(bam1_t));
+
+    *b = bnew;
+    *ba = n;
+
+    return 0;
+}
+
+// We have b[0]..b[bn-1] entries all from the same template (qname)
+typedef struct {
+    bam1_t *b;
+    int n, ba;  // number used and number allocated
+    int b_next; // b[b_next] for start of next set, -1 if unset
+    int eof;    // marker for having seen eof
+} bam_set;
+
+// Fetches a new batch of BAM records all containing the same name.
+// NB: we cache the last (non-matching) name in b[n], so we can use it to
+// start the next batch.
+// Returns the number of records on success,
+//         <0 on failure or EOF (sam_read1 return vals)
+static int next_template(samFile *in, sam_hdr_t *header, bam_set *bs,
+                         int sanitize_flags) {
+    int result;
+
+    if (bs->eof)
+        return -1;
+
+    // First time through, prime the template name
+    if (bs->b_next < 0) {
+        if (grow_b_array(&bs->b, &bs->ba, 1) < 0)
+            return -2;
+        result = sam_read1(in, header, &bs->b[0]);
+        if (result < 0)
+            return result;
+        if (bam_sanitize(header, &bs->b[0], sanitize_flags) < 0)
+            return -2;
+    } else {
+        // Otherwise use the previous template name read
+        bam1_t btmp = bs->b[0];
+        bs->b[0] = bs->b[bs->b_next];
+        bs->b[bs->b_next] = btmp; // For ->{,l_,m_}data
+    }
+    bs->n = 1;
+
+    // Now keep reading until we find a read that mismatches or we hit eof.
+    char *name = bam_get_qname(&bs->b[0]);
+    for (;;) {
+        if (grow_b_array(&bs->b, &bs->ba, bs->n+1) < 0)
+            return -2;
+
+        result = sam_read1(in, header, &bs->b[bs->n]);
+        if (result < -1)
+            return result;
+
+        if (result < 0) {
+            bs->eof = 1;
+            bs->b_next = -1;
+            break;
+        } else {
+            if (bam_sanitize(header, &bs->b[bs->n], sanitize_flags) < 0)
+                return -2;
+
+            bs->b_next = bs->n;
+            if (strcmp(name, bam_get_qname(&bs->b[bs->n])) != 0)
+                break;
+        }
+
+        bs->n++;
+    }
+
+    return bs->n;
+}
+
 // currently, this function ONLY works if each read has one hit
+//
+// Returns 0 on success,
+//        >0 on failure
 static int bam_mating_core(samFile *in, samFile *out, int remove_reads,
                            int proper_pair_check, int add_ct,
                            int do_mate_scoring, char *arg_list, int no_pg,
-                           int sanitize_flags)
+                           int sanitize_flags, int base_mods)
 {
     sam_hdr_t *header;
-    bam1_t *b[2] = { NULL, NULL };
-    int curr, has_prev, result;
-    hts_pos_t pre_end = 0, cur_end = 0;
+    int result, n;
     kstring_t str = KS_INITIALIZE;
+    bam_set bs = {NULL, 0, 0, -1, 0};
 
     header = sam_hdr_read(in);
     if (header == NULL) {
@@ -508,101 +987,140 @@ static int bam_mating_core(samFile *in, samFile *out, int remove_reads,
 
     if (sam_hdr_write(out, header) < 0) goto write_fail;
 
-    b[0] = bam_init1();
-    b[1] = bam_init1();
-    curr = 0; has_prev = 0;
-    while ((result = sam_read1(in, header, b[curr])) >= 0) {
-        bam1_t *cur = b[curr], *pre = b[1-curr];
-        if (bam_sanitize(header, cur, sanitize_flags) < 0)
-            goto fail;
-        if (cur->core.flag & BAM_FSECONDARY)
-        {
-            if ( !remove_reads ) {
-                if (sam_write1(out, header, cur) < 0) goto write_fail;
+    // Iterate template by template fetching bs->n records at a time
+    while ((result = next_template(in, header, &bs, sanitize_flags)) >= 0) {
+        bam1_t *cur = NULL, *pre = NULL, *rnum[2] = {NULL, NULL};
+        int prev = -1, curr = -1;
+        hts_pos_t pre_end = 0, cur_end = 0;
+
+        // Find and fix up primary alignments
+        MM_state state[2];
+        for (n = 0; n < bs.n; n++) {
+            int is_r2 = (bs.b[n].core.flag & BAM_FREAD2) != 0;
+            if (bs.b[n].core.flag & (BAM_FSECONDARY | BAM_FSUPPLEMENTARY))
+                continue;
+
+            if (base_mods)
+                if (fix_MM(NULL, &bs.b[n], &state[is_r2]) < 0)
+                    goto fail;
+
+            if (!pre) {
+                pre = &bs.b[prev = n];
+                rnum[(pre->core.flag & BAM_FREAD2) != 0] = pre;
+
+                pre_end = (pre->core.flag & BAM_FUNMAP) == 0
+                    ? bam_endpos(pre) : 0;
+                continue;
             }
-            continue; // skip secondary alignments
-        }
-        if (cur->core.flag & BAM_FSUPPLEMENTARY)
-        {
-            if (sam_write1(out, header, cur) < 0) goto write_fail;
-            continue; // pass supplementary alignments through unchanged (TODO:make them match read they came from)
-        }
-        if ((cur->core.flag&BAM_FUNMAP) == 0) // If mapped calculate end
-        {
-            cur_end = bam_endpos(cur);
-        }
 
-        if (has_prev) { // do we have a pair of reads to examine?
-            if (strcmp(bam_get_qname(cur), bam_get_qname(pre)) == 0) { // identical pair name
-                pre->core.flag |= BAM_FPAIRED;
-                cur->core.flag |= BAM_FPAIRED;
-                if (sync_mate(pre, cur)) goto fail;
-
-                if (pre->core.tid == cur->core.tid && !(cur->core.flag&(BAM_FUNMAP|BAM_FMUNMAP))
-                    && !(pre->core.flag&(BAM_FUNMAP|BAM_FMUNMAP))) // if safe set TLEN/ISIZE
-                {
-                    hts_pos_t cur5, pre5;
-                    cur5 = (cur->core.flag&BAM_FREVERSE)? cur_end : cur->core.pos;
-                    pre5 = (pre->core.flag&BAM_FREVERSE)? pre_end : pre->core.pos;
-                    cur->core.isize = pre5 - cur5; pre->core.isize = cur5 - pre5;
-                } else cur->core.isize = pre->core.isize = 0;
-                if (add_ct) bam_template_cigar(pre, cur, &str);
-                // TODO: Add code to properly check if read is in a proper pair based on ISIZE distribution
-                if (proper_pair_check && !plausibly_properly_paired(pre,cur)) {
-                    pre->core.flag &= ~BAM_FPROPER_PAIR;
-                    cur->core.flag &= ~BAM_FPROPER_PAIR;
-                }
+            // Note, more than 2 primary alignments will use 'curr' as last
+            cur = &bs.b[curr = n];
+            rnum[(cur->core.flag & BAM_FREAD2) != 0] = cur;
+            cur_end = (cur->core.flag & BAM_FUNMAP) == 0
+                ? bam_endpos(cur) : 0;
+
+            pre->core.flag |= BAM_FPAIRED;
+            cur->core.flag |= BAM_FPAIRED;
+            if (sync_mate(pre, cur))
+                goto fail;
+
+            // If safe set TLEN/ISIZE
+            if (pre->core.tid == cur->core.tid
+                && !(cur->core.flag & (BAM_FUNMAP | BAM_FMUNMAP))
+                && !(pre->core.flag & (BAM_FUNMAP | BAM_FMUNMAP))) {
+                hts_pos_t cur5, pre5;
+                cur5 = (cur->core.flag & BAM_FREVERSE)
+                    ? cur_end
+                    : cur->core.pos;
+                pre5 = (pre->core.flag & BAM_FREVERSE)
+                    ? pre_end
+                    : pre->core.pos;
+                cur->core.isize = pre5 - cur5;
+                pre->core.isize = cur5 - pre5;
+            } else {
+                cur->core.isize = pre->core.isize = 0;
+            }
 
-                if (do_mate_scoring) {
-                    if ((add_mate_score(pre, cur) == -1) || (add_mate_score(cur, pre) == -1)) {
-                        fprintf(samtools_stderr, "[bam_mating_core] ERROR: unable to add mate score.\n");
-                        goto fail;
-                    }
-                }
+            if (add_ct)
+                bam_template_cigar(pre, cur, &str);
 
-                // Write out result
-                if ( !remove_reads ) {
-                    if (sam_write1(out, header, pre) < 0) goto write_fail;
-                    if (sam_write1(out, header, cur) < 0) goto write_fail;
-                } else {
-                    // If we have to remove reads make sure we do it in a way that doesn't create orphans with bad flags
-                    if(pre->core.flag&BAM_FUNMAP) cur->core.flag &= ~(BAM_FPAIRED|BAM_FMREVERSE|BAM_FPROPER_PAIR);
-                    if(cur->core.flag&BAM_FUNMAP) pre->core.flag &= ~(BAM_FPAIRED|BAM_FMREVERSE|BAM_FPROPER_PAIR);
-                    if(!(pre->core.flag&BAM_FUNMAP)) {
-                        if (sam_write1(out, header, pre) < 0) goto write_fail;
-                    }
-                    if(!(cur->core.flag&BAM_FUNMAP)) {
-                        if (sam_write1(out, header, cur) < 0) goto write_fail;
-                    }
-                }
-                has_prev = 0;
-            } else { // unpaired?  clear bad info and write it out
-                pre->core.mtid = -1; pre->core.mpos = -1; pre->core.isize = 0;
-                pre->core.flag &= ~(BAM_FPAIRED|BAM_FMREVERSE|BAM_FPROPER_PAIR);
-                if ( !remove_reads || !(pre->core.flag&BAM_FUNMAP) ) {
-                    if (sam_write1(out, header, pre) < 0) goto write_fail;
+            // TODO: Add code to properly check if read is in a proper
+            // pair based on ISIZE distribution
+            if (proper_pair_check && !plausibly_properly_paired(pre,cur)) {
+                pre->core.flag &= ~BAM_FPROPER_PAIR;
+                cur->core.flag &= ~BAM_FPROPER_PAIR;
+            }
+
+            if (do_mate_scoring) {
+                if ((add_mate_score(pre, cur) == -1) ||
+                    (add_mate_score(cur, pre) == -1)) {
+                    fprintf(samtools_stderr, "[bam_mating_core] ERROR: "
+                            "unable to add mate score.\n");
+                    goto fail;
                 }
             }
-        } else has_prev = 1;
-        curr = 1 - curr;
-        pre_end = cur_end;
-    }
-    if (result < -1) goto read_fail;
-    if (has_prev && !remove_reads) { // If we still have a BAM in the buffer it must be unpaired
-        bam1_t *pre = b[1-curr];
-        if (pre->core.tid < 0 || pre->core.pos < 0 || pre->core.flag&BAM_FUNMAP) { // If unmapped
-            pre->core.flag |= BAM_FUNMAP;
-            pre->core.tid = -1;
-            pre->core.pos = -1;
+
+            // If we have to remove reads make sure we do it in a way that
+            // doesn't create orphans with bad flags
+            if (remove_reads) {
+                if (pre->core.flag&BAM_FUNMAP)
+                    cur->core.flag &=
+                        ~(BAM_FMREVERSE|BAM_FPROPER_PAIR);
+                if (cur->core.flag&BAM_FUNMAP)
+                    pre->core.flag &=
+                        ~(BAM_FMREVERSE|BAM_FPROPER_PAIR);
+            }
         }
-        pre->core.mtid = -1; pre->core.mpos = -1; pre->core.isize = 0;
-        pre->core.flag &= ~(BAM_FPAIRED|BAM_FMREVERSE|BAM_FPROPER_PAIR);
 
-        if (sam_write1(out, header, pre) < 0) goto write_fail;
+        // Handle unpaired primary data
+        if (!cur && pre) {
+            pre->core.mtid = -1;
+            pre->core.mpos = -1;
+            pre->core.isize = 0;
+            pre->core.flag &= ~(BAM_FMREVERSE|BAM_FPROPER_PAIR);
+        }
+
+        // Now process secondary and supplementary alignments
+        for (n = 0; n < bs.n; n++) {
+            if (!(bs.b[n].core.flag & (BAM_FSECONDARY|BAM_FSUPPLEMENTARY))) {
+                // primary
+                continue;
+            }
+
+            // Secondary or supplementary
+            int is_r2 = (bs.b[n].core.flag & BAM_FREAD2) != 0;
+            bam1_t *primary = rnum[is_r2];
+            if (primary) {
+                if (base_mods)
+                    fix_MM(primary, &bs.b[n], &state[is_r2]);
+            } else {
+                // Record with base modifications but no known primary
+                //fprintf(samtools_stderr, "Unpaired secondary or supplementary\n");
+                if (base_mods)
+                    fix_MM(NULL, &bs.b[n], NULL);
+            }
+        }
+
+        // Finally having curated everything, write out all records in their
+        // original ordering
+        for (n = 0; n < bs.n; n++) {
+            bam1_t *cur = &bs.b[n];
+            // We may remove unmapped and secondary alignments
+            if (remove_reads && (cur->core.flag & (BAM_FSECONDARY|BAM_FUNMAP)))
+                continue;
+
+            if (sam_write1(out, header, cur) < 0)
+                goto write_fail;
+        }
     }
+    if (result < -1)
+        goto read_fail;
+
     sam_hdr_destroy(header);
-    bam_destroy1(b[0]);
-    bam_destroy1(b[1]);
+
+    for (n = 0; n < bs.ba; n++)
+        free(bs.b[n].data);
+    free(bs.b);
     ks_free(&str);
     return 0;
 
@@ -614,8 +1132,9 @@ static int bam_mating_core(samFile *in, samFile *out, int remove_reads,
     print_error_errno("fixmate", "Couldn't write to output file");
  fail:
     sam_hdr_destroy(header);
-    bam_destroy1(b[0]);
-    bam_destroy1(b[1]);
+    for (n = 0; n < bs.ba; n++)
+        free(bs.b[n].data);
+    free(bs.b);
     ks_free(&str);
     return 1;
 }
@@ -632,6 +1151,7 @@ void usage(FILE* where)
 "  -u           Uncompressed output\n"
 "  -z, --sanitize FLAG[,FLAG]\n"
 "               Sanitize alignment fields [defaults to all types]\n"
+"  -M           Fix base modification tags (MM/ML/MN)\n"
 "  --no-PG      do not add a PG line\n");
 
     sam_global_opt_help(where, "-.O..@-.");
@@ -648,7 +1168,7 @@ int bam_mating(int argc, char *argv[])
     htsThreadPool p = {NULL, 0};
     samFile *in = NULL, *out = NULL;
     int c, remove_reads = 0, proper_pair_check = 1, add_ct = 0, res = 1,
-        mate_score = 0, no_pg = 0, sanitize_flags = FIX_ALL;
+        mate_score = 0, no_pg = 0, sanitize_flags = FIX_ALL, base_mods = 0;
     sam_global_args ga = SAM_GLOBAL_ARGS_INIT;
     char wmode[4] = {'w', 'b', 0, 0};
     static const struct option lopts[] = {
@@ -660,12 +1180,13 @@ int bam_mating(int argc, char *argv[])
 
     // parse args
     if (argc == 1) { usage(samtools_stdout); return 0; }
-    while ((c = getopt_long(argc, argv, "rpcmO:@:uz:", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "rpcmMO:@:uz:", lopts, NULL)) >= 0) {
         switch (c) {
         case 'r': remove_reads = 1; break;
         case 'p': proper_pair_check = 0; break;
         case 'c': add_ct = 1; break;
         case 'm': mate_score = 1; break;
+        case 'M': base_mods = 1; break;
         case 'u': wmode[2] = '0'; break;
         case 1: no_pg = 1; break;
         default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
@@ -704,7 +1225,8 @@ int bam_mating(int argc, char *argv[])
 
     // run
     res = bam_mating_core(in, out, remove_reads, proper_pair_check, add_ct,
-                          mate_score, arg_list, no_pg, sanitize_flags);
+                          mate_score, arg_list, no_pg, sanitize_flags,
+                          base_mods);
 
     // cleanup
     sam_close(in);
index b9182b6a8915ddf7a189cd9d57263c46f94f1a1e..55d02dc5392831c00bce57c90acde8d9244b1cb4 100644 (file)
@@ -328,7 +328,7 @@ static void refs_destroy(ref_cache *cache) {
     }
 }
 
-int calmd_usage() {
+int calmd_usage(void) {
     fprintf(stderr,
 "Usage: samtools calmd [-eubrAESQ] <aln.bam> <ref.fasta>\n"
 "Options:\n"
index 795eccb21c5074892b692c924f475cb314cad60c..e1101475b86348e026b463e8076d20e148447f4a 100644 (file)
@@ -330,7 +330,7 @@ static void refs_destroy(ref_cache *cache) {
     }
 }
 
-int calmd_usage() {
+int calmd_usage(void) {
     fprintf(samtools_stderr,
 "Usage: samtools calmd [-eubrAESQ] <aln.bam> <ref.fasta>\n"
 "Options:\n"
index 9a718e01c3e7ab2e4903721821b5b383e56b81a2..ba76db18fcb93879f7e08bb593d74f1abe45a4d5 100644 (file)
@@ -50,11 +50,6 @@ void bam_plbuf_destroy(bam_plbuf_t *buf);
 
 int bam_plbuf_push(const bam1_t *b, bam_plbuf_t *buf);
 
-/* Exported from bam_plcmd.c */
-int pileup_seq(FILE *fp, const bam_pileup1_t *p, hts_pos_t pos,
-               hts_pos_t ref_len, const char *ref, kstring_t *ks,
-               int rev_del, int no_ins, int no_ins_mods,
-               int no_del, int no_ends);
 #ifdef __cplusplus
 }
 #endif
index 264a7f5a086f9351c66e629c319d54c15137eadb..4880cd420efad490272704fc3244e34ae5c246db 100644 (file)
@@ -1,6 +1,6 @@
 /*  bam_plcmd.c -- mpileup subcommand.
 
-    Copyright (C) 2008-2015, 2019-2021 Genome Research Ltd.
+    Copyright (C) 2008-2015, 2019-2021, 2023-2024 Genome Research Ltd.
     Portions copyright (C) 2009-2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -51,51 +51,43 @@ DEALINGS IN THE SOFTWARE.  */
 #define dummy_free(p)
 KLIST_INIT(auxlist, char *, dummy_free)
 
-static inline int printw(int c, FILE *fp)
-{
-    char buf[16];
-    int l, x;
-    if (c == 0) return fputc('0', fp);
-    for (l = 0, x = c < 0? -c : c; x > 0; x /= 10) buf[l++] = x%10 + '0';
-    if (c < 0) buf[l++] = '-';
-    buf[l] = 0;
-    for (x = 0; x < l/2; ++x) {
-        int y = buf[x]; buf[x] = buf[l-1-x]; buf[l-1-x] = y;
-    }
-    fputs(buf, fp);
-    return 0;
-}
-
-int pileup_seq(FILE *fp, const bam_pileup1_t *p, hts_pos_t pos,
-               hts_pos_t ref_len, const char *ref, kstring_t *ks,
+int pileup_seq(kstring_t *ks_seq, const bam_pileup1_t *p, hts_pos_t pos,
+               hts_pos_t ref_len, const char *ref, kstring_t *ks_mod,
                int rev_del, int no_ins, int no_ins_mods,
                int no_del, int no_ends)
 {
     no_ins_mods |= no_ins;
-    int j;
+    int j, err = 0;
     hts_base_mod_state *m = p->cd.p;
     if (!no_ends && p->is_head) {
-        putc('^', fp);
-        putc(p->b->core.qual > 93? 126 : p->b->core.qual + 33, fp);
+        err |= kputc_('^', ks_seq) < 0;
+        err |= kputc_(p->b->core.qual > 93 ? 126 : p->b->core.qual + 33,
+                      ks_seq) < 0;
     }
     if (!p->is_del) {
+        // See seq_nt16_str in htslib/hts.c
+        const char seq_nt_str_lc[] = ",acmgrsvtwyhkdbn"; // reverse strand
+        const char seq_nt_str_uc[] = ".ACMGRSVTWYHKDBN";
         int c = p->qpos < p->b->core.l_qseq
-            ? seq_nt16_str[bam_seqi(bam_get_seq(p->b), p->qpos)]
-            : 'N';
+            ? bam_seqi(bam_get_seq(p->b), p->qpos)
+            : 15 /*N*/;
         if (ref) {
-            int rb = pos < ref_len? ref[pos] : 'N';
-            if (c == '=' || seq_nt16_table[c] == seq_nt16_table[rb]) c = bam_is_rev(p->b)? ',' : '.';
-            else c = bam_is_rev(p->b)? tolower(c) : toupper(c);
-        } else {
-            if (c == '=') c = bam_is_rev(p->b)? ',' : '.';
-            else c = bam_is_rev(p->b)? tolower(c) : toupper(c);
+            int rb = pos < ref_len
+                ? seq_nt16_table[(uint8_t)(ref[pos])]
+                : 15/*N*/;
+            if (c == rb)
+                c = 0; // "=", which becomes . or ,
         }
-        putc(c, fp);
+        c = bam_is_rev(p->b)
+            ? seq_nt_str_lc[c]
+            : seq_nt_str_uc[c];
+        err |= kputc_(c, ks_seq) < 0;
+
         if (m) {
             int nm;
             hts_base_mod mod[256];
             if ((nm = bam_mods_at_qpos(p->b, p->qpos, m, mod, 256)) > 0) {
-                putc('[', fp);
+                err |= kputc_('[', ks_seq) < 0;
                 int j;
                 for (j = 0; j < nm && j < 256; j++) {
                     char qual[20];
@@ -105,61 +97,75 @@ int pileup_seq(FILE *fp, const bam_pileup1_t *p, hts_pos_t pos,
                         *qual = 0;
                     if (mod[j].modified_base < 0)
                         // ChEBI
-                        fprintf(fp, "%c(%d)%s", "+-"[mod[j].strand],
-                                -mod[j].modified_base, qual);
+                        err |= ksprintf(ks_seq, "%c(%d)%s",
+                                        "+-"[mod[j].strand],
+                                        -mod[j].modified_base, qual) < 0;
                     else
-                        fprintf(fp, "%c%c%s", "+-"[mod[j].strand],
-                                mod[j].modified_base, qual);
+                        err |= ksprintf(ks_seq, "%c%c%s", "+-"[mod[j].strand],
+                                        mod[j].modified_base, qual) < 0;
                 }
-                putc(']', fp);
+                err |= kputc_(']', ks_seq) < 0;
             }
         }
-    } else putc(p->is_refskip? (bam_is_rev(p->b)? '<' : '>') : ((bam_is_rev(p->b) && rev_del) ? '#' : '*'), fp);
+    } else {
+        err |= kputc_(p->is_refskip
+                      ? (bam_is_rev(p->b)? '<' : '>')
+                      : ((bam_is_rev(p->b) && rev_del) ? '#' : '*'),
+                      ks_seq) < 0;
+    }
+
     int del_len = -p->indel;
     if (p->indel > 0) {
         int len = bam_plp_insertion_mod(p, m && !no_ins_mods ? m : NULL,
-                                        ks, &del_len);
+                                        ks_mod, &del_len);
         if (len < 0) {
             print_error("mpileup", "bam_plp_insertion() failed");
             return -1;
         }
         if (no_ins < 2) {
-            putc('+', fp);
-            printw(len, fp);
+            err |= kputc_('+', ks_seq) < 0;
+            err |= kputuw(len, ks_seq) < 0;
         }
         if (!no_ins) {
+            kstring_t *ks = ks_mod;
             if (bam_is_rev(p->b)) {
                 char pad = rev_del ? '#' : '*';
                 int in_mod = 0;
                 for (j = 0; j < ks->l; j++) {
                     if (ks->s[j] == '[') in_mod = 1;
                     else if (ks->s[j] == ']') in_mod = 0;
-                    putc(ks->s[j] != '*'
-                         ? (in_mod ? ks->s[j] : tolower(ks->s[j]))
-                         : pad, fp);
+                    err |= kputc_(ks->s[j] != '*'
+                                  ? (in_mod ? ks->s[j] : tolower(ks->s[j]))
+                                  : pad, ks_seq) < 0;
                 }
             } else {
                 int in_mod = 0;
                 for (j = 0; j < ks->l; j++) {
                     if (ks->s[j] == '[') in_mod = 1;
                     if (ks->s[j] == ']') in_mod = 0;
-                    putc(in_mod ? ks->s[j] : toupper(ks->s[j]), fp);
+                    err |= kputc_(in_mod ? ks->s[j] : toupper(ks->s[j]),
+                                  ks_seq) < 0;
                 }
             }
         }
     }
+
     if (del_len > 0) {
         if (no_del < 2)
-            printw(-del_len, fp);
+            err |= kputw(-del_len, ks_seq) < 0;
         if (!no_del) {
             for (j = 1; j <= del_len; ++j) {
                 int c = (ref && (int)pos+j < ref_len)? ref[pos+j] : 'N';
-                putc(bam_is_rev(p->b)? tolower(c) : toupper(c), fp);
+                err |= kputc_(bam_is_rev(p->b)? tolower(c) : toupper(c),
+                              ks_seq) < 0;
             }
         }
     }
-    if (!no_ends && p->is_tail) putc('$', fp);
-    return 0;
+
+    if (!no_ends && p->is_tail)
+        err |= kputc_('$', ks_seq) < 0;
+
+    return -err;
 }
 
 #include "sample.h"
@@ -175,6 +181,7 @@ int pileup_seq(FILE *fp, const bam_pileup1_t *p, hts_pos_t pos,
 
 #define MPLP_PRINT_MAPQ_CHAR (1<<11)
 #define MPLP_PRINT_QPOS  (1<<12)
+// Start of struct active_cols elements
 #define MPLP_PRINT_QNAME (1<<13)
 #define MPLP_PRINT_FLAG  (1<<14)
 #define MPLP_PRINT_RNAME (1<<15)
@@ -186,10 +193,12 @@ int pileup_seq(FILE *fp, const bam_pileup1_t *p, hts_pos_t pos,
 #define MPLP_PRINT_TLEN  (1<<21)
 #define MPLP_PRINT_SEQ   (1<<22)
 #define MPLP_PRINT_QUAL  (1<<23)
-#define MPLP_PRINT_MODS  (1<<24)
-#define MPLP_PRINT_QPOS5 (1<<25)
+#define MPLP_PRINT_RLEN  (1<<24)
+// Must occur after struct active_cols element list
+#define MPLP_PRINT_MODS  (1<<25)
+#define MPLP_PRINT_QPOS5 (1<<26)
 
-#define MPLP_PRINT_LAST  (1<<26) // terminator for loop
+#define MPLP_PRINT_LAST  (1<<27) // terminator for loop
 
 #define MPLP_MAX_DEPTH 8000
 #define MPLP_MAX_INDEL_DEPTH 250
@@ -241,11 +250,13 @@ static int build_auxlist(mplp_conf_t *conf, char *optstring) {
         int supported;
     };
 
-    const struct active_cols colnames[11] = {
-            {"QNAME", 1}, {"FLAG", 1}, {"RNAME", 1}, {"POS", 1}, {"MAPQ", 1}, {"CIGAR", 0}, {"RNEXT", 1}, {"PNEXT", 1}, {"TLEN", 0}, {"SEQ", 0}, {"QUAL", 0}
+    const struct active_cols colnames[12] = {
+            {"QNAME", 1}, {"FLAG", 1}, {"RNAME", 1}, {"POS", 1}, {"MAPQ", 1},
+            {"CIGAR", 0}, {"RNEXT", 1}, {"PNEXT", 1}, {"TLEN", 0}, {"SEQ", 0},
+            {"QUAL", 0},  {"RLEN", 1},
     };
 
-    int i, f = MPLP_PRINT_QNAME, colno = 11;
+    int i, f = MPLP_PRINT_QNAME, colno = sizeof(colnames)/sizeof(*colnames);
     for (i = 0; i < colno; i++, f <<= 1)
         if (colnames[i].supported)
             khash_str2int_set(colhash, colnames[i].name, f);
@@ -440,7 +451,7 @@ static int mplp_func(void *data, bam1_t *b)
  * @param fn filenames
  * @param fn_idx index filenames
  */
-static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
+static int mpileup(mplp_conf_t *conf, int nfn, char **fn, char **fn_idx)
 {
     mplp_aux_t **data;
     int i, tid, *n_plp, tid0 = 0, max_depth;
@@ -458,19 +469,19 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
 
     memset(&gplp, 0, sizeof(mplp_pileup_t));
     memset(&buf, 0, sizeof(kstring_t));
-    data = calloc(n, sizeof(mplp_aux_t*));
-    plp = calloc(n, sizeof(bam_pileup1_t*));
-    n_plp = calloc(n, sizeof(int));
+    data = calloc(nfn, sizeof(mplp_aux_t*));
+    plp = calloc(nfn, sizeof(bam_pileup1_t*));
+    n_plp = calloc(nfn, sizeof(int));
     sm = bam_smpl_init();
 
-    if (n == 0) {
+    if (nfn == 0) {
         fprintf(stderr,"[%s] no input file/data given\n", __func__);
         exit(EXIT_FAILURE);
     }
 
     // read the header of each file in the list and initialize data
     refs_t *refs = NULL;
-    for (i = 0; i < n; ++i) {
+    for (i = 0; i < nfn; ++i) {
         sam_hdr_t *h_tmp;
         data[i] = calloc(1, sizeof(mplp_aux_t));
         data[i]->fp = sam_open_format(fn[i], "rb", &conf->ga.in);
@@ -540,7 +551,8 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
             data[i]->h = h;
         }
     }
-    fprintf(stderr, "[%s] %d samples in %d input files\n", __func__, sm->n, n);
+    fprintf(stderr, "[%s] %d samples in %d input files\n",
+            __func__, sm->n, nfn);
 
     pileup_fp = conf->output_fname? fopen(conf->output_fname, "w") : stdout;
 
@@ -550,7 +562,7 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
     }
 
     // init pileup
-    iter = bam_mplp_init(n, mplp_func, (void**)data);
+    iter = bam_mplp_init(nfn, mplp_func, (void**)data);
     if (conf->flag & MPLP_PRINT_MODS) {
         bam_mplp_constructor(iter, pileup_cd_create);
         bam_mplp_destructor(iter, pileup_cd_destroy);
@@ -561,7 +573,7 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
         fprintf(stderr, "[%s] Max depth set to maximum value (%d)\n", __func__, INT_MAX);
     } else {
         max_depth = conf->max_depth;
-        if ( max_depth * n > 1<<20 )
+        if ( max_depth * nfn > 1<<20 )
             fprintf(stderr, "[%s] Combined max depth is above 1M. Potential memory hog!\n", __func__);
     }
 
@@ -570,12 +582,15 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
     int ret;
     int last_tid = -1;
     hts_pos_t last_pos = -1;
+    int one_seq = 0;
 
     // begin pileup
+    kstring_t ks_seq = KS_INITIALIZE;
+    kstring_t ks_mod = KS_INITIALIZE;
+    kstring_t ks_qual = KS_INITIALIZE;
     while ( (ret=bam_mplp64_auto(iter, &tid, &pos, n_plp, plp)) > 0) {
+        one_seq = 1; // at least 1 output
         if (conf->reg && (pos < beg0 || pos >= end0)) continue; // out of the region requested
-        mplp_get_ref(data[0], tid, &ref, &ref_len);
-        //printf("tid=%d len=%d ref=%p/%s\n", tid, ref_len, ref, ref);
         if (conf->all) {
             // Deal with missing portions of previous tids
             while (tid > last_tid) {
@@ -583,22 +598,27 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
                     while (++last_pos < sam_hdr_tid2len(h, last_tid)) {
                         if (conf->bed && bed_overlap(conf->bed, sam_hdr_tid2name(h, last_tid), last_pos, last_pos + 1) == 0)
                             continue;
-                        print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, last_tid), last_pos, n, ref, ref_len);
+                        print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, last_tid), last_pos, nfn, ref, ref_len);
                     }
                 }
                 last_tid++;
                 last_pos = -1;
                 if (conf->all < 2)
                     break;
+                if (tid > last_tid)
+                    // multiple missing references and -aa used
+                    mplp_get_ref(data[0], last_tid, &ref, &ref_len);
             }
         }
+        mplp_get_ref(data[0], tid, &ref, &ref_len);
+
         if (conf->all) {
             // Deal with missing portion of current tid
             while (++last_pos < pos) {
                 if (conf->reg && last_pos < beg0) continue; // out of range; skip
                 if (conf->bed && bed_overlap(conf->bed, sam_hdr_tid2name(h, tid), last_pos, last_pos + 1) == 0)
                     continue;
-                print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, tid), last_pos, n, ref, ref_len);
+                print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, tid), last_pos, nfn, ref, ref_len);
             }
             last_tid = tid;
             last_pos = pos;
@@ -606,16 +626,34 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
         if (conf->bed && tid >= 0 && !bed_overlap(conf->bed, sam_hdr_tid2name(h, tid), pos, pos+1)) continue;
 
         fprintf(pileup_fp, "%s\t%"PRIhts_pos"\t%c", sam_hdr_tid2name(h, tid), pos + 1, (ref && pos < ref_len)? ref[pos] : 'N');
-        for (i = 0; i < n; ++i) {
-            int j, cnt;
+        for (i = 0; i < nfn; ++i) {
+            int j, cnt, err = 0;
+            ks_clear(&ks_seq);
+            ks_clear(&ks_qual);
+            ks_clear(&ks_mod);
             for (j = cnt = 0; j < n_plp[i]; ++j) {
                 const bam_pileup1_t *p = plp[i] + j;
                 int c = p->qpos < p->b->core.l_qseq
                     ? bam_get_qual(p->b)[p->qpos]
                     : 0;
-                if (c >= conf->min_baseQ) ++cnt;
+                if (c >= conf->min_baseQ) {
+                    // Build up seq
+                    err |= pileup_seq(&ks_seq, plp[i] + j, pos, ref_len,
+                                      ref, &ks_mod, conf->rev_del,
+                                      conf->no_ins, conf->no_ins_mods,
+                                      conf->no_del, conf->no_ends) < 0;
+
+                    // Build up qual
+                    err |= kputc_(c+33 < 126 ? c+33 : 126, &ks_qual) < 0;
+                    cnt++;
+                }
+            }
+            if (err) {
+                ret = 1;
+                goto fail;
             }
             fprintf(pileup_fp, "\t%d\t", cnt);
+
             if (n_plp[i] == 0) {
                 fputs("*\t*", pileup_fp);
                 int flag_value = MPLP_PRINT_MAPQ_CHAR;
@@ -631,49 +669,25 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
                         fputs("\t*", pileup_fp);
                 }
             } else {
-                int n = 0;
-                kstring_t ks = KS_INITIALIZE;
-                for (j = 0; j < n_plp[i]; ++j) {
-                    const bam_pileup1_t *p = plp[i] + j;
-                    int c = p->qpos < p->b->core.l_qseq
-                        ? bam_get_qual(p->b)[p->qpos]
-                        : 0;
-                    if (c >= conf->min_baseQ) {
-                        n++;
-                        if (pileup_seq(pileup_fp, plp[i] + j, pos, ref_len,
-                                       ref, &ks, conf->rev_del,
-                                       conf->no_ins, conf->no_ins_mods,
-                                       conf->no_del, conf->no_ends) < 0) {
-                            ret = 1;
-                            goto fail;
-                        }
-                    }
+                if (ks_seq.l) {
+                    fwrite(ks_seq.s, 1, ks_seq.l, pileup_fp);
+                } else {
+                    putc('*', pileup_fp);
                 }
-                if (!n) putc('*', pileup_fp);
-
-                /* Print base qualities */
-                n = 0;
-                ks_free(&ks);
                 putc('\t', pileup_fp);
-                for (j = 0; j < n_plp[i]; ++j) {
-                    const bam_pileup1_t *p = plp[i] + j;
-                    int c = p->qpos < p->b->core.l_qseq
-                        ? bam_get_qual(p->b)[p->qpos]
-                        : 0;
-                    if (c >= conf->min_baseQ) {
-                        c = c + 33 < 126? c + 33 : 126;
-                        putc(c, pileup_fp);
-                        n++;
-                    }
+
+                if (ks_qual.l) {
+                    fwrite(ks_qual.s, 1, ks_qual.l, pileup_fp);
+                } else {
+                    putc('*', pileup_fp);
                 }
-                if (!n) putc('*', pileup_fp);
 
                 /* Print selected columns */
                 int flag_value = MPLP_PRINT_MAPQ_CHAR;
                 while(flag_value < MPLP_PRINT_LAST) {
                     if (flag_value != MPLP_PRINT_MODS
                         && (conf->flag & flag_value)) {
-                        n = 0;
+                        int n = 0;
                         putc('\t', pileup_fp);
                         for (j = 0; j < n_plp[i]; ++j) {
                             const bam_pileup1_t *p = &plp[i][j];
@@ -729,6 +743,9 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
                             case MPLP_PRINT_PNEXT:
                                 fprintf(pileup_fp, "%"PRId64, (int64_t) p->b->core.mpos + 1);
                                 break;
+                            case MPLP_PRINT_RLEN:
+                                fprintf(pileup_fp, "%d", p->b->core.l_qseq);
+                                break;
                             }
                         }
                         if (!n) putc('*', pileup_fp);
@@ -741,7 +758,7 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
                 if (auxlist_p && auxlist_p->size) {
                     kliter_t(auxlist) *aux;
                     for (aux = kl_begin(auxlist_p); aux != kl_end(auxlist_p); aux = kl_next(aux)) {
-                        n = 0;
+                        int n = 0; // NB shadows outer loop
                         putc('\t', pileup_fp);
                         for (j = 0; j < n_plp[i]; ++j) {
                             const bam_pileup1_t *p = &plp[i][j];
@@ -799,6 +816,10 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
         putc('\n', pileup_fp);
     }
 
+    ks_free(&ks_seq);
+    ks_free(&ks_mod);
+    ks_free(&ks_qual);
+
     if (ret < 0) {
         print_error("mpileup", "error reading from input file");
         ret = EXIT_FAILURE;
@@ -811,13 +832,16 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
             last_tid = tid0;
             last_pos = beg0-1;
             mplp_get_ref(data[0], tid0, &ref, &ref_len);
+        } else if (last_tid < 0 && !one_seq && conf->all > 1) {
+            last_tid = 0; // --aa on a blank file
         }
-       while (last_tid >= 0 && last_tid < sam_hdr_nref(h)) {
+        while (last_tid >= 0 && last_tid < sam_hdr_nref(h)) {
+            mplp_get_ref(data[0], last_tid, &ref, &ref_len);
             while (++last_pos < sam_hdr_tid2len(h, last_tid)) {
                 if (last_pos >= end0) break;
                 if (conf->bed && bed_overlap(conf->bed, sam_hdr_tid2name(h, last_tid), last_pos, last_pos + 1) == 0)
                     continue;
-                print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, last_tid), last_pos, n, ref, ref_len);
+                print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, last_tid), last_pos, nfn, ref, ref_len);
             }
             last_tid++;
             last_pos = -1;
@@ -834,7 +858,7 @@ fail:
     free(gplp.plp); free(gplp.n_plp); free(gplp.m_plp);
     bam_mplp_destroy(iter);
     sam_hdr_destroy(h);
-    for (i = 0; i < n; ++i) {
+    for (i = 0; i < nfn; ++i) {
         sam_close(data[i]->fp);
         if (data[i]->iter) hts_itr_destroy(data[i]->iter);
         free(data[i]);
index 009867e07c2ef42c72bbbbacf569ba65cfead8c3..2b6f018212ec516510798adc85abc27ed119e36b 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  bam_plcmd.c -- mpileup subcommand.
 
-    Copyright (C) 2008-2015, 2019-2021 Genome Research Ltd.
+    Copyright (C) 2008-2015, 2019-2021, 2023-2024 Genome Research Ltd.
     Portions copyright (C) 2009-2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -53,51 +53,43 @@ DEALINGS IN THE SOFTWARE.  */
 #define dummy_free(p)
 KLIST_INIT(auxlist, char *, dummy_free)
 
-static inline int printw(int c, FILE *fp)
-{
-    char buf[16];
-    int l, x;
-    if (c == 0) return fputc('0', fp);
-    for (l = 0, x = c < 0? -c : c; x > 0; x /= 10) buf[l++] = x%10 + '0';
-    if (c < 0) buf[l++] = '-';
-    buf[l] = 0;
-    for (x = 0; x < l/2; ++x) {
-        int y = buf[x]; buf[x] = buf[l-1-x]; buf[l-1-x] = y;
-    }
-    fputs(buf, fp);
-    return 0;
-}
-
-int pileup_seq(FILE *fp, const bam_pileup1_t *p, hts_pos_t pos,
-               hts_pos_t ref_len, const char *ref, kstring_t *ks,
+int pileup_seq(kstring_t *ks_seq, const bam_pileup1_t *p, hts_pos_t pos,
+               hts_pos_t ref_len, const char *ref, kstring_t *ks_mod,
                int rev_del, int no_ins, int no_ins_mods,
                int no_del, int no_ends)
 {
     no_ins_mods |= no_ins;
-    int j;
+    int j, err = 0;
     hts_base_mod_state *m = p->cd.p;
     if (!no_ends && p->is_head) {
-        putc('^', fp);
-        putc(p->b->core.qual > 93? 126 : p->b->core.qual + 33, fp);
+        err |= kputc_('^', ks_seq) < 0;
+        err |= kputc_(p->b->core.qual > 93 ? 126 : p->b->core.qual + 33,
+                      ks_seq) < 0;
     }
     if (!p->is_del) {
+        // See seq_nt16_str in htslib/hts.c
+        const char seq_nt_str_lc[] = ",acmgrsvtwyhkdbn"; // reverse strand
+        const char seq_nt_str_uc[] = ".ACMGRSVTWYHKDBN";
         int c = p->qpos < p->b->core.l_qseq
-            ? seq_nt16_str[bam_seqi(bam_get_seq(p->b), p->qpos)]
-            : 'N';
+            ? bam_seqi(bam_get_seq(p->b), p->qpos)
+            : 15 /*N*/;
         if (ref) {
-            int rb = pos < ref_len? ref[pos] : 'N';
-            if (c == '=' || seq_nt16_table[c] == seq_nt16_table[rb]) c = bam_is_rev(p->b)? ',' : '.';
-            else c = bam_is_rev(p->b)? tolower(c) : toupper(c);
-        } else {
-            if (c == '=') c = bam_is_rev(p->b)? ',' : '.';
-            else c = bam_is_rev(p->b)? tolower(c) : toupper(c);
+            int rb = pos < ref_len
+                ? seq_nt16_table[(uint8_t)(ref[pos])]
+                : 15/*N*/;
+            if (c == rb)
+                c = 0; // "=", which becomes . or ,
         }
-        putc(c, fp);
+        c = bam_is_rev(p->b)
+            ? seq_nt_str_lc[c]
+            : seq_nt_str_uc[c];
+        err |= kputc_(c, ks_seq) < 0;
+
         if (m) {
             int nm;
             hts_base_mod mod[256];
             if ((nm = bam_mods_at_qpos(p->b, p->qpos, m, mod, 256)) > 0) {
-                putc('[', fp);
+                err |= kputc_('[', ks_seq) < 0;
                 int j;
                 for (j = 0; j < nm && j < 256; j++) {
                     char qual[20];
@@ -107,61 +99,75 @@ int pileup_seq(FILE *fp, const bam_pileup1_t *p, hts_pos_t pos,
                         *qual = 0;
                     if (mod[j].modified_base < 0)
                         // ChEBI
-                        fprintf(fp, "%c(%d)%s", "+-"[mod[j].strand],
-                                -mod[j].modified_base, qual);
+                        err |= ksprintf(ks_seq, "%c(%d)%s",
+                                        "+-"[mod[j].strand],
+                                        -mod[j].modified_base, qual) < 0;
                     else
-                        fprintf(fp, "%c%c%s", "+-"[mod[j].strand],
-                                mod[j].modified_base, qual);
+                        err |= ksprintf(ks_seq, "%c%c%s", "+-"[mod[j].strand],
+                                        mod[j].modified_base, qual) < 0;
                 }
-                putc(']', fp);
+                err |= kputc_(']', ks_seq) < 0;
             }
         }
-    } else putc(p->is_refskip? (bam_is_rev(p->b)? '<' : '>') : ((bam_is_rev(p->b) && rev_del) ? '#' : '*'), fp);
+    } else {
+        err |= kputc_(p->is_refskip
+                      ? (bam_is_rev(p->b)? '<' : '>')
+                      : ((bam_is_rev(p->b) && rev_del) ? '#' : '*'),
+                      ks_seq) < 0;
+    }
+
     int del_len = -p->indel;
     if (p->indel > 0) {
         int len = bam_plp_insertion_mod(p, m && !no_ins_mods ? m : NULL,
-                                        ks, &del_len);
+                                        ks_mod, &del_len);
         if (len < 0) {
             print_error("mpileup", "bam_plp_insertion() failed");
             return -1;
         }
         if (no_ins < 2) {
-            putc('+', fp);
-            printw(len, fp);
+            err |= kputc_('+', ks_seq) < 0;
+            err |= kputuw(len, ks_seq) < 0;
         }
         if (!no_ins) {
+            kstring_t *ks = ks_mod;
             if (bam_is_rev(p->b)) {
                 char pad = rev_del ? '#' : '*';
                 int in_mod = 0;
                 for (j = 0; j < ks->l; j++) {
                     if (ks->s[j] == '[') in_mod = 1;
                     else if (ks->s[j] == ']') in_mod = 0;
-                    putc(ks->s[j] != '*'
-                         ? (in_mod ? ks->s[j] : tolower(ks->s[j]))
-                         : pad, fp);
+                    err |= kputc_(ks->s[j] != '*'
+                                  ? (in_mod ? ks->s[j] : tolower(ks->s[j]))
+                                  : pad, ks_seq) < 0;
                 }
             } else {
                 int in_mod = 0;
                 for (j = 0; j < ks->l; j++) {
                     if (ks->s[j] == '[') in_mod = 1;
                     if (ks->s[j] == ']') in_mod = 0;
-                    putc(in_mod ? ks->s[j] : toupper(ks->s[j]), fp);
+                    err |= kputc_(in_mod ? ks->s[j] : toupper(ks->s[j]),
+                                  ks_seq) < 0;
                 }
             }
         }
     }
+
     if (del_len > 0) {
         if (no_del < 2)
-            printw(-del_len, fp);
+            err |= kputw(-del_len, ks_seq) < 0;
         if (!no_del) {
             for (j = 1; j <= del_len; ++j) {
                 int c = (ref && (int)pos+j < ref_len)? ref[pos+j] : 'N';
-                putc(bam_is_rev(p->b)? tolower(c) : toupper(c), fp);
+                err |= kputc_(bam_is_rev(p->b)? tolower(c) : toupper(c),
+                              ks_seq) < 0;
             }
         }
     }
-    if (!no_ends && p->is_tail) putc('$', fp);
-    return 0;
+
+    if (!no_ends && p->is_tail)
+        err |= kputc_('$', ks_seq) < 0;
+
+    return -err;
 }
 
 #include "sample.h"
@@ -177,6 +183,7 @@ int pileup_seq(FILE *fp, const bam_pileup1_t *p, hts_pos_t pos,
 
 #define MPLP_PRINT_MAPQ_CHAR (1<<11)
 #define MPLP_PRINT_QPOS  (1<<12)
+// Start of struct active_cols elements
 #define MPLP_PRINT_QNAME (1<<13)
 #define MPLP_PRINT_FLAG  (1<<14)
 #define MPLP_PRINT_RNAME (1<<15)
@@ -188,10 +195,12 @@ int pileup_seq(FILE *fp, const bam_pileup1_t *p, hts_pos_t pos,
 #define MPLP_PRINT_TLEN  (1<<21)
 #define MPLP_PRINT_SEQ   (1<<22)
 #define MPLP_PRINT_QUAL  (1<<23)
-#define MPLP_PRINT_MODS  (1<<24)
-#define MPLP_PRINT_QPOS5 (1<<25)
+#define MPLP_PRINT_RLEN  (1<<24)
+// Must occur after struct active_cols element list
+#define MPLP_PRINT_MODS  (1<<25)
+#define MPLP_PRINT_QPOS5 (1<<26)
 
-#define MPLP_PRINT_LAST  (1<<26) // terminator for loop
+#define MPLP_PRINT_LAST  (1<<27) // terminator for loop
 
 #define MPLP_MAX_DEPTH 8000
 #define MPLP_MAX_INDEL_DEPTH 250
@@ -243,11 +252,13 @@ static int build_auxlist(mplp_conf_t *conf, char *optstring) {
         int supported;
     };
 
-    const struct active_cols colnames[11] = {
-            {"QNAME", 1}, {"FLAG", 1}, {"RNAME", 1}, {"POS", 1}, {"MAPQ", 1}, {"CIGAR", 0}, {"RNEXT", 1}, {"PNEXT", 1}, {"TLEN", 0}, {"SEQ", 0}, {"QUAL", 0}
+    const struct active_cols colnames[12] = {
+            {"QNAME", 1}, {"FLAG", 1}, {"RNAME", 1}, {"POS", 1}, {"MAPQ", 1},
+            {"CIGAR", 0}, {"RNEXT", 1}, {"PNEXT", 1}, {"TLEN", 0}, {"SEQ", 0},
+            {"QUAL", 0},  {"RLEN", 1},
     };
 
-    int i, f = MPLP_PRINT_QNAME, colno = 11;
+    int i, f = MPLP_PRINT_QNAME, colno = sizeof(colnames)/sizeof(*colnames);
     for (i = 0; i < colno; i++, f <<= 1)
         if (colnames[i].supported)
             khash_str2int_set(colhash, colnames[i].name, f);
@@ -442,7 +453,7 @@ static int mplp_func(void *data, bam1_t *b)
  * @param fn filenames
  * @param fn_idx index filenames
  */
-static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
+static int mpileup(mplp_conf_t *conf, int nfn, char **fn, char **fn_idx)
 {
     mplp_aux_t **data;
     int i, tid, *n_plp, tid0 = 0, max_depth;
@@ -460,19 +471,19 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
 
     memset(&gplp, 0, sizeof(mplp_pileup_t));
     memset(&buf, 0, sizeof(kstring_t));
-    data = calloc(n, sizeof(mplp_aux_t*));
-    plp = calloc(n, sizeof(bam_pileup1_t*));
-    n_plp = calloc(n, sizeof(int));
+    data = calloc(nfn, sizeof(mplp_aux_t*));
+    plp = calloc(nfn, sizeof(bam_pileup1_t*));
+    n_plp = calloc(nfn, sizeof(int));
     sm = bam_smpl_init();
 
-    if (n == 0) {
+    if (nfn == 0) {
         fprintf(samtools_stderr,"[%s] no input file/data given\n", __func__);
         samtools_exit(EXIT_FAILURE);
     }
 
     // read the header of each file in the list and initialize data
     refs_t *refs = NULL;
-    for (i = 0; i < n; ++i) {
+    for (i = 0; i < nfn; ++i) {
         sam_hdr_t *h_tmp;
         data[i] = calloc(1, sizeof(mplp_aux_t));
         data[i]->fp = sam_open_format(fn[i], "rb", &conf->ga.in);
@@ -542,7 +553,8 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
             data[i]->h = h;
         }
     }
-    fprintf(samtools_stderr, "[%s] %d samples in %d input files\n", __func__, sm->n, n);
+    fprintf(samtools_stderr, "[%s] %d samples in %d input files\n",
+            __func__, sm->n, nfn);
 
     pileup_fp = conf->output_fname? fopen(conf->output_fname, "w") : samtools_stdout;
 
@@ -552,7 +564,7 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
     }
 
     // init pileup
-    iter = bam_mplp_init(n, mplp_func, (void**)data);
+    iter = bam_mplp_init(nfn, mplp_func, (void**)data);
     if (conf->flag & MPLP_PRINT_MODS) {
         bam_mplp_constructor(iter, pileup_cd_create);
         bam_mplp_destructor(iter, pileup_cd_destroy);
@@ -563,7 +575,7 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
         fprintf(samtools_stderr, "[%s] Max depth set to maximum value (%d)\n", __func__, INT_MAX);
     } else {
         max_depth = conf->max_depth;
-        if ( max_depth * n > 1<<20 )
+        if ( max_depth * nfn > 1<<20 )
             fprintf(samtools_stderr, "[%s] Combined max depth is above 1M. Potential memory hog!\n", __func__);
     }
 
@@ -572,12 +584,15 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
     int ret;
     int last_tid = -1;
     hts_pos_t last_pos = -1;
+    int one_seq = 0;
 
     // begin pileup
+    kstring_t ks_seq = KS_INITIALIZE;
+    kstring_t ks_mod = KS_INITIALIZE;
+    kstring_t ks_qual = KS_INITIALIZE;
     while ( (ret=bam_mplp64_auto(iter, &tid, &pos, n_plp, plp)) > 0) {
+        one_seq = 1; // at least 1 output
         if (conf->reg && (pos < beg0 || pos >= end0)) continue; // out of the region requested
-        mplp_get_ref(data[0], tid, &ref, &ref_len);
-        //printf("tid=%d len=%d ref=%p/%s\n", tid, ref_len, ref, ref);
         if (conf->all) {
             // Deal with missing portions of previous tids
             while (tid > last_tid) {
@@ -585,22 +600,27 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
                     while (++last_pos < sam_hdr_tid2len(h, last_tid)) {
                         if (conf->bed && bed_overlap(conf->bed, sam_hdr_tid2name(h, last_tid), last_pos, last_pos + 1) == 0)
                             continue;
-                        print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, last_tid), last_pos, n, ref, ref_len);
+                        print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, last_tid), last_pos, nfn, ref, ref_len);
                     }
                 }
                 last_tid++;
                 last_pos = -1;
                 if (conf->all < 2)
                     break;
+                if (tid > last_tid)
+                    // multiple missing references and -aa used
+                    mplp_get_ref(data[0], last_tid, &ref, &ref_len);
             }
         }
+        mplp_get_ref(data[0], tid, &ref, &ref_len);
+
         if (conf->all) {
             // Deal with missing portion of current tid
             while (++last_pos < pos) {
                 if (conf->reg && last_pos < beg0) continue; // out of range; skip
                 if (conf->bed && bed_overlap(conf->bed, sam_hdr_tid2name(h, tid), last_pos, last_pos + 1) == 0)
                     continue;
-                print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, tid), last_pos, n, ref, ref_len);
+                print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, tid), last_pos, nfn, ref, ref_len);
             }
             last_tid = tid;
             last_pos = pos;
@@ -608,16 +628,34 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
         if (conf->bed && tid >= 0 && !bed_overlap(conf->bed, sam_hdr_tid2name(h, tid), pos, pos+1)) continue;
 
         fprintf(pileup_fp, "%s\t%"PRIhts_pos"\t%c", sam_hdr_tid2name(h, tid), pos + 1, (ref && pos < ref_len)? ref[pos] : 'N');
-        for (i = 0; i < n; ++i) {
-            int j, cnt;
+        for (i = 0; i < nfn; ++i) {
+            int j, cnt, err = 0;
+            ks_clear(&ks_seq);
+            ks_clear(&ks_qual);
+            ks_clear(&ks_mod);
             for (j = cnt = 0; j < n_plp[i]; ++j) {
                 const bam_pileup1_t *p = plp[i] + j;
                 int c = p->qpos < p->b->core.l_qseq
                     ? bam_get_qual(p->b)[p->qpos]
                     : 0;
-                if (c >= conf->min_baseQ) ++cnt;
+                if (c >= conf->min_baseQ) {
+                    // Build up seq
+                    err |= pileup_seq(&ks_seq, plp[i] + j, pos, ref_len,
+                                      ref, &ks_mod, conf->rev_del,
+                                      conf->no_ins, conf->no_ins_mods,
+                                      conf->no_del, conf->no_ends) < 0;
+
+                    // Build up qual
+                    err |= kputc_(c+33 < 126 ? c+33 : 126, &ks_qual) < 0;
+                    cnt++;
+                }
+            }
+            if (err) {
+                ret = 1;
+                goto fail;
             }
             fprintf(pileup_fp, "\t%d\t", cnt);
+
             if (n_plp[i] == 0) {
                 fputs("*\t*", pileup_fp);
                 int flag_value = MPLP_PRINT_MAPQ_CHAR;
@@ -633,49 +671,25 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
                         fputs("\t*", pileup_fp);
                 }
             } else {
-                int n = 0;
-                kstring_t ks = KS_INITIALIZE;
-                for (j = 0; j < n_plp[i]; ++j) {
-                    const bam_pileup1_t *p = plp[i] + j;
-                    int c = p->qpos < p->b->core.l_qseq
-                        ? bam_get_qual(p->b)[p->qpos]
-                        : 0;
-                    if (c >= conf->min_baseQ) {
-                        n++;
-                        if (pileup_seq(pileup_fp, plp[i] + j, pos, ref_len,
-                                       ref, &ks, conf->rev_del,
-                                       conf->no_ins, conf->no_ins_mods,
-                                       conf->no_del, conf->no_ends) < 0) {
-                            ret = 1;
-                            goto fail;
-                        }
-                    }
+                if (ks_seq.l) {
+                    fwrite(ks_seq.s, 1, ks_seq.l, pileup_fp);
+                } else {
+                    putc('*', pileup_fp);
                 }
-                if (!n) putc('*', pileup_fp);
-
-                /* Print base qualities */
-                n = 0;
-                ks_free(&ks);
                 putc('\t', pileup_fp);
-                for (j = 0; j < n_plp[i]; ++j) {
-                    const bam_pileup1_t *p = plp[i] + j;
-                    int c = p->qpos < p->b->core.l_qseq
-                        ? bam_get_qual(p->b)[p->qpos]
-                        : 0;
-                    if (c >= conf->min_baseQ) {
-                        c = c + 33 < 126? c + 33 : 126;
-                        putc(c, pileup_fp);
-                        n++;
-                    }
+
+                if (ks_qual.l) {
+                    fwrite(ks_qual.s, 1, ks_qual.l, pileup_fp);
+                } else {
+                    putc('*', pileup_fp);
                 }
-                if (!n) putc('*', pileup_fp);
 
                 /* Print selected columns */
                 int flag_value = MPLP_PRINT_MAPQ_CHAR;
                 while(flag_value < MPLP_PRINT_LAST) {
                     if (flag_value != MPLP_PRINT_MODS
                         && (conf->flag & flag_value)) {
-                        n = 0;
+                        int n = 0;
                         putc('\t', pileup_fp);
                         for (j = 0; j < n_plp[i]; ++j) {
                             const bam_pileup1_t *p = &plp[i][j];
@@ -731,6 +745,9 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
                             case MPLP_PRINT_PNEXT:
                                 fprintf(pileup_fp, "%"PRId64, (int64_t) p->b->core.mpos + 1);
                                 break;
+                            case MPLP_PRINT_RLEN:
+                                fprintf(pileup_fp, "%d", p->b->core.l_qseq);
+                                break;
                             }
                         }
                         if (!n) putc('*', pileup_fp);
@@ -743,7 +760,7 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
                 if (auxlist_p && auxlist_p->size) {
                     kliter_t(auxlist) *aux;
                     for (aux = kl_begin(auxlist_p); aux != kl_end(auxlist_p); aux = kl_next(aux)) {
-                        n = 0;
+                        int n = 0; // NB shadows outer loop
                         putc('\t', pileup_fp);
                         for (j = 0; j < n_plp[i]; ++j) {
                             const bam_pileup1_t *p = &plp[i][j];
@@ -801,6 +818,10 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
         putc('\n', pileup_fp);
     }
 
+    ks_free(&ks_seq);
+    ks_free(&ks_mod);
+    ks_free(&ks_qual);
+
     if (ret < 0) {
         print_error("mpileup", "error reading from input file");
         ret = EXIT_FAILURE;
@@ -813,13 +834,16 @@ static int mpileup(mplp_conf_t *conf, int n, char **fn, char **fn_idx)
             last_tid = tid0;
             last_pos = beg0-1;
             mplp_get_ref(data[0], tid0, &ref, &ref_len);
+        } else if (last_tid < 0 && !one_seq && conf->all > 1) {
+            last_tid = 0; // --aa on a blank file
         }
-       while (last_tid >= 0 && last_tid < sam_hdr_nref(h)) {
+        while (last_tid >= 0 && last_tid < sam_hdr_nref(h)) {
+            mplp_get_ref(data[0], last_tid, &ref, &ref_len);
             while (++last_pos < sam_hdr_tid2len(h, last_tid)) {
                 if (last_pos >= end0) break;
                 if (conf->bed && bed_overlap(conf->bed, sam_hdr_tid2name(h, last_tid), last_pos, last_pos + 1) == 0)
                     continue;
-                print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, last_tid), last_pos, n, ref, ref_len);
+                print_empty_pileup(pileup_fp, conf, sam_hdr_tid2name(h, last_tid), last_pos, nfn, ref, ref_len);
             }
             last_tid++;
             last_pos = -1;
@@ -836,7 +860,7 @@ fail:
     free(gplp.plp); free(gplp.n_plp); free(gplp.m_plp);
     bam_mplp_destroy(iter);
     sam_hdr_destroy(h);
-    for (i = 0; i < n; ++i) {
+    for (i = 0; i < nfn; ++i) {
         sam_close(data[i]->fp);
         if (data[i]->iter) hts_itr_destroy(data[i]->iter);
         free(data[i]);
index b44bd665de8eb92120b82be280e7bcb323c625b5..2ff6f7e953d6a0ab1f277937b648eb63b4ebc1a4 100644 (file)
@@ -1,6 +1,6 @@
 /*  bam_sort.c -- sorting and merging.
 
-    Copyright (C) 2008-2023 Genome Research Ltd.
+    Copyright (C) 2008-2024 Genome Research Ltd.
     Portions copyright (C) 2009-2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -164,11 +164,15 @@ static template_coordinate_key_t* template_coordinate_key(bam1_t *b, template_co
 
 typedef enum {Coordinate, QueryName, TagCoordinate, TagQueryName, MinHash, TemplateCoordinate} SamOrder;
 static SamOrder g_sam_order = Coordinate;
+static int natural_sort = 1; // not ASCII, but alphanumeric: a12b > a7b
 static char g_sort_tag[2] = {0,0};
 
 #define is_digit(c) ((c)<='9' && (c)>='0')
 static int strnum_cmp(const char *_a, const char *_b)
 {
+    if (!natural_sort)
+        return strcmp(_a,_b);
+
     const unsigned char *a = (const unsigned char*)_a, *b = (const unsigned char*)_b;
     const unsigned char *pa = a, *pb = b;
     while (*pa && *pb) {
@@ -236,8 +240,12 @@ static inline int heap_lt(const heap1_t a, const heap1_t b)
         case QueryName:
             t = strnum_cmp(bam_get_qname(a.entry.bam_record), bam_get_qname(b.entry.bam_record));
             if (t != 0) return t > 0;
-            fa = a.entry.bam_record->core.flag & 0xc0;
-            fb = b.entry.bam_record->core.flag & 0xc0;
+            fa = a.entry.bam_record->core.flag;
+            fb = b.entry.bam_record->core.flag;
+            // Sort order is READ1, READ2, (PRIMARY), SUPPLEMENTARY, SECONDARY
+            // Get the bits in this order so sort is a natural a-b
+            fa = ((fa&0xc0)<<8)|((fa&0x100)<<3)|((fa&0x800)>>3);
+            fb = ((fb&0xc0)<<8)|((fb&0x100)<<3)|((fb&0x800)>>3);
             if (fa != fb) return fa > fb;
             break;
         case TagQueryName:
@@ -258,7 +266,7 @@ static inline int heap_lt(const heap1_t a, const heap1_t b)
             break;
     }
 
-    // This compares by position in the input file(s)
+    // This compares by position (i/idx'th read) in the input file(s)
     if (a.i != b.i) return a.i > b.i;
     return a.idx > b.idx;
 }
@@ -320,7 +328,7 @@ static void trans_tbl_destroy(trans_tbl_t *tbl) {
  *  Create a merged_header_t struct.
  */
 
-static merged_header_t * init_merged_header() {
+static merged_header_t * init_merged_header(void) {
     merged_header_t *merged_hdr;
 
     merged_hdr = calloc(1, sizeof(*merged_hdr));
@@ -397,7 +405,7 @@ static int gen_unique_id(char *prefix, khash_t(cset) *existing_ids,
 
     do {
         dest->l = 0;
-        ksprintf(dest, "%s-%0lX", prefix, lrand48());
+        ksprintf(dest, "%s-%08lX", prefix, lrand48());
         iter = kh_get(cset, existing_ids, ks_str(dest));
     } while (iter != kh_end(existing_ids));
 
@@ -1320,7 +1328,7 @@ int bam_merge_core2(SamOrder sam_order, char* sort_tag, const char *out, const c
     // Make sure that there's enough memory for template coordinate keys, one per file to read
     if (sam_order == TemplateCoordinate) {
         if ((keys = malloc(sizeof(template_coordinate_keys_t))) == NULL) {
-            print_error("sort", "could not allocate memory for the top-level keys");
+            print_error(cmd, "could not allocate memory for the top-level keys");
             goto mem_fail;
         }
         keys->n = 0;
@@ -1356,8 +1364,8 @@ int bam_merge_core2(SamOrder sam_order, char* sort_tag, const char *out, const c
                 h->entry.u.tag = bam_aux_get(h->entry.bam_record, g_sort_tag);
             } else if (g_sam_order == TemplateCoordinate) {
                 template_coordinate_key_t *key = template_coordinate_keys_get(keys, i); // get the next key to use
-                h->entry.u.key = template_coordinate_key(heap->entry.bam_record, key, hout, lib_lookup); // update the key
-                if (heap->entry.u.key == NULL) goto mem_fail; // key could not be created, error out
+                h->entry.u.key = template_coordinate_key(h->entry.bam_record, key, hout, lib_lookup); // update the key
+                if (h->entry.u.key == NULL) goto fail; // key could not be created, error out
             } else {
                 h->entry.u.tag = NULL;
             }
@@ -1431,7 +1439,7 @@ int bam_merge_core2(SamOrder sam_order, char* sort_tag, const char *out, const c
             } else if (g_sam_order == TemplateCoordinate) {
                 template_coordinate_key_t *key = template_coordinate_keys_get(keys, heap->i); // get the next key to use
                 heap->entry.u.key = template_coordinate_key(heap->entry.bam_record, key, hout, lib_lookup); // update the key
-                if (heap->entry.u.key == NULL) goto mem_fail; // key could not be created, error out
+                if (heap->entry.u.key == NULL) goto fail; // key could not be created, error out
             } else {
                 heap->entry.u.tag = NULL;
             }
@@ -1473,9 +1481,17 @@ int bam_merge_core2(SamOrder sam_order, char* sort_tag, const char *out, const c
     bed_destroy(hreg);
     free(RG); free(translation_tbl); free(fp); free(heap); free(iter); free(hdr);
     if (sam_close(fpout) < 0) {
-        print_error(cmd, "error closing output file");
+        print_error_errno(cmd, "error closing output file \"%s\"", out);
         return -1;
     }
+    if (keys != NULL) {
+        for (i = 0; i < keys->m; ++i) {
+            free(keys->buffers[i]);
+        }
+        free(keys->buffers);
+        free(keys);
+    }
+    lib_lookup_destroy(lib_lookup);
     return 0;
 
  mem_fail:
@@ -1535,7 +1551,8 @@ static void merge_usage(FILE *to)
 "   or: samtools merge [options] <out.bam> <in1.bam> ... <inN.bam>\n"
 "\n"
 "Options:\n"
-"  -n         Input files are sorted by read name\n"
+"  -n         Input files are sorted by read name (natural)\n"
+"  -N         Input files are sorted by read name (ASCII)\n"
 "  -t TAG     Input files are sorted by TAG value\n"
 "  -r         Attach RG tag (inferred from file names)\n"
 "  -u         Uncompressed BAM output\n"
@@ -1581,11 +1598,12 @@ int bam_merge(int argc, char *argv[])
         return 0;
     }
 
-    while ((c = getopt_long(argc, argv, "h:nru1R:o:f@:l:cps:b:O:t:XL:", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "h:nNru1R:o:f@:l:cps:b:O:t:XL:", lopts, NULL)) >= 0) {
         switch (c) {
         case 'r': flag |= MERGE_RG; break;
         case 'f': flag |= MERGE_FORCE; break;
         case 'h': fn_headers = optarg; break;
+        case 'N': natural_sort = 0; // fall through
         case 'n': sam_order = QueryName; break;
         case 'o': fnout = optarg; break;
         case 't': sort_tag = optarg; break;
@@ -1924,7 +1942,7 @@ static int bam_merge_simple(SamOrder sam_order, char *sort_tag, const char *out,
     }
 
     if (sam_close(fpout) < 0) {
-        print_error(cmd, "error closing output file");
+        print_error_errno(cmd, "error closing output file \"%s\"", out);
         return -1;
     }
     return 0;
@@ -1959,7 +1977,13 @@ static inline int bam1_cmp_core(const bam1_tag a, const bam1_tag b)
     if (g_sam_order == QueryName || g_sam_order == TagQueryName) {
         int t = strnum_cmp(bam_get_qname(a.bam_record), bam_get_qname(b.bam_record));
         if (t != 0) return t;
-        return (int) (a.bam_record->core.flag&0xc0) - (int) (b.bam_record->core.flag&0xc0);
+        int af = a.bam_record->core.flag;
+        int bf = b.bam_record->core.flag;
+        // Sort order is READ1, READ2, (PRIMARY), SUPPLEMENTARY, SECONDARY
+        // Get the bits in this order so sort is a natural a-b
+        af = ((af&0xc0)<<8)|((af&0x100)<<3)|((af&0x800)>>3);
+        bf = ((bf&0xc0)<<8)|((bf&0x100)<<3)|((bf&0x800)>>3);
+        return af - bf;
     } else {
         pa = a.bam_record->core.tid;
         pb = b.bam_record->core.tid;
@@ -3055,7 +3079,7 @@ static void *worker(void *data)
             break;
         case MinHash:
             worker_minhash(w);
-            // no break, go to merge sort
+            // fall-through
         default:
             ks_mergesort(sort, w->buf_len, w->buf, 0);
     }
@@ -3287,6 +3311,9 @@ int bam_sort_core_ext(SamOrder sam_order, char* sort_tag, int minimiser_kmer,
             break;
         case QueryName:
             new_so = "queryname";
+            new_ss = natural_sort
+                ? "queryname:natural"
+                : "queryname:lexicographical";
             break;
         case MinHash:
             new_so = "coordinate";
@@ -3605,7 +3632,8 @@ static void sort_usage(FILE *fp)
 "  -I FILE    Order minimisers by their position in FILE FASTA\n"
 "  -w INT     Window size for minimiser indexing via -I ref.fa [100]\n"
 "  -H         Squash homopolymers when computing minimiser\n"
-"  -n         Sort by read name (not compatible with samtools index command)\n"
+"  -n         Sort by read name (natural): cannot be used with samtools index\n"
+"  -N         Sort by read name (ASCII): cannot be used with samtools index\n"
 "  -t TAG     Sort by value of TAG. Uses position as secondary index (or read name if -n is set)\n"
 "  -o FILE    Write final output to FILE rather than standard output\n"
 "  -T PREFIX  Write temporary files to PREFIX.nnnn.bam\n"
@@ -3658,9 +3686,10 @@ int bam_sort(int argc, char *argv[])
         { NULL, 0, NULL, 0 }
     };
 
-    while ((c = getopt_long(argc, argv, "l:m:no:O:T:@:t:MI:K:uRw:H", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "l:m:nNo:O:T:@:t:MI:K:uRw:H", lopts, NULL)) >= 0) {
         switch (c) {
         case 'o': fnout = optarg; o_seen = 1; break;
+        case 'N': natural_sort = 0; // fall through
         case 'n': sam_order = QueryName; break;
         case 't': by_tag = true; sort_tag = optarg; break;
         case 'm': {
@@ -3732,7 +3761,7 @@ int bam_sort(int argc, char *argv[])
         goto sort_end;
     }
 
-    if (ga.write_index && (sam_order == QueryName || sam_order == TagQueryName || sam_order == TagCoordinate || sam_order == TemplateCoordinate)) {
+    if (ga.write_index && sam_order != Coordinate) {
         fprintf(stderr, "[W::bam_sort] Ignoring --write-index as it only works for position sorted files.\n");
         ga.write_index = 0;
     }
index 80aa4d8076d7abea0994be7684bb43159d85e690..3aa3a49afac5b98dc31ad94251916d23103ced94 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  bam_sort.c -- sorting and merging.
 
-    Copyright (C) 2008-2023 Genome Research Ltd.
+    Copyright (C) 2008-2024 Genome Research Ltd.
     Portions copyright (C) 2009-2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -166,11 +166,15 @@ static template_coordinate_key_t* template_coordinate_key(bam1_t *b, template_co
 
 typedef enum {Coordinate, QueryName, TagCoordinate, TagQueryName, MinHash, TemplateCoordinate} SamOrder;
 static SamOrder g_sam_order = Coordinate;
+static int natural_sort = 1; // not ASCII, but alphanumeric: a12b > a7b
 static char g_sort_tag[2] = {0,0};
 
 #define is_digit(c) ((c)<='9' && (c)>='0')
 static int strnum_cmp(const char *_a, const char *_b)
 {
+    if (!natural_sort)
+        return strcmp(_a,_b);
+
     const unsigned char *a = (const unsigned char*)_a, *b = (const unsigned char*)_b;
     const unsigned char *pa = a, *pb = b;
     while (*pa && *pb) {
@@ -238,8 +242,12 @@ static inline int heap_lt(const heap1_t a, const heap1_t b)
         case QueryName:
             t = strnum_cmp(bam_get_qname(a.entry.bam_record), bam_get_qname(b.entry.bam_record));
             if (t != 0) return t > 0;
-            fa = a.entry.bam_record->core.flag & 0xc0;
-            fb = b.entry.bam_record->core.flag & 0xc0;
+            fa = a.entry.bam_record->core.flag;
+            fb = b.entry.bam_record->core.flag;
+            // Sort order is READ1, READ2, (PRIMARY), SUPPLEMENTARY, SECONDARY
+            // Get the bits in this order so sort is a natural a-b
+            fa = ((fa&0xc0)<<8)|((fa&0x100)<<3)|((fa&0x800)>>3);
+            fb = ((fb&0xc0)<<8)|((fb&0x100)<<3)|((fb&0x800)>>3);
             if (fa != fb) return fa > fb;
             break;
         case TagQueryName:
@@ -260,7 +268,7 @@ static inline int heap_lt(const heap1_t a, const heap1_t b)
             break;
     }
 
-    // This compares by position in the input file(s)
+    // This compares by position (i/idx'th read) in the input file(s)
     if (a.i != b.i) return a.i > b.i;
     return a.idx > b.idx;
 }
@@ -322,7 +330,7 @@ static void trans_tbl_destroy(trans_tbl_t *tbl) {
  *  Create a merged_header_t struct.
  */
 
-static merged_header_t * init_merged_header() {
+static merged_header_t * init_merged_header(void) {
     merged_header_t *merged_hdr;
 
     merged_hdr = calloc(1, sizeof(*merged_hdr));
@@ -399,7 +407,7 @@ static int gen_unique_id(char *prefix, khash_t(cset) *existing_ids,
 
     do {
         dest->l = 0;
-        ksprintf(dest, "%s-%0lX", prefix, lrand48());
+        ksprintf(dest, "%s-%08lX", prefix, lrand48());
         iter = kh_get(cset, existing_ids, ks_str(dest));
     } while (iter != kh_end(existing_ids));
 
@@ -1322,7 +1330,7 @@ int bam_merge_core2(SamOrder sam_order, char* sort_tag, const char *out, const c
     // Make sure that there's enough memory for template coordinate keys, one per file to read
     if (sam_order == TemplateCoordinate) {
         if ((keys = malloc(sizeof(template_coordinate_keys_t))) == NULL) {
-            print_error("sort", "could not allocate memory for the top-level keys");
+            print_error(cmd, "could not allocate memory for the top-level keys");
             goto mem_fail;
         }
         keys->n = 0;
@@ -1358,8 +1366,8 @@ int bam_merge_core2(SamOrder sam_order, char* sort_tag, const char *out, const c
                 h->entry.u.tag = bam_aux_get(h->entry.bam_record, g_sort_tag);
             } else if (g_sam_order == TemplateCoordinate) {
                 template_coordinate_key_t *key = template_coordinate_keys_get(keys, i); // get the next key to use
-                h->entry.u.key = template_coordinate_key(heap->entry.bam_record, key, hout, lib_lookup); // update the key
-                if (heap->entry.u.key == NULL) goto mem_fail; // key could not be created, error out
+                h->entry.u.key = template_coordinate_key(h->entry.bam_record, key, hout, lib_lookup); // update the key
+                if (h->entry.u.key == NULL) goto fail; // key could not be created, error out
             } else {
                 h->entry.u.tag = NULL;
             }
@@ -1433,7 +1441,7 @@ int bam_merge_core2(SamOrder sam_order, char* sort_tag, const char *out, const c
             } else if (g_sam_order == TemplateCoordinate) {
                 template_coordinate_key_t *key = template_coordinate_keys_get(keys, heap->i); // get the next key to use
                 heap->entry.u.key = template_coordinate_key(heap->entry.bam_record, key, hout, lib_lookup); // update the key
-                if (heap->entry.u.key == NULL) goto mem_fail; // key could not be created, error out
+                if (heap->entry.u.key == NULL) goto fail; // key could not be created, error out
             } else {
                 heap->entry.u.tag = NULL;
             }
@@ -1475,9 +1483,17 @@ int bam_merge_core2(SamOrder sam_order, char* sort_tag, const char *out, const c
     bed_destroy(hreg);
     free(RG); free(translation_tbl); free(fp); free(heap); free(iter); free(hdr);
     if (sam_close(fpout) < 0) {
-        print_error(cmd, "error closing output file");
+        print_error_errno(cmd, "error closing output file \"%s\"", out);
         return -1;
     }
+    if (keys != NULL) {
+        for (i = 0; i < keys->m; ++i) {
+            free(keys->buffers[i]);
+        }
+        free(keys->buffers);
+        free(keys);
+    }
+    lib_lookup_destroy(lib_lookup);
     return 0;
 
  mem_fail:
@@ -1537,7 +1553,8 @@ static void merge_usage(FILE *to)
 "   or: samtools merge [options] <out.bam> <in1.bam> ... <inN.bam>\n"
 "\n"
 "Options:\n"
-"  -n         Input files are sorted by read name\n"
+"  -n         Input files are sorted by read name (natural)\n"
+"  -N         Input files are sorted by read name (ASCII)\n"
 "  -t TAG     Input files are sorted by TAG value\n"
 "  -r         Attach RG tag (inferred from file names)\n"
 "  -u         Uncompressed BAM output\n"
@@ -1583,11 +1600,12 @@ int bam_merge(int argc, char *argv[])
         return 0;
     }
 
-    while ((c = getopt_long(argc, argv, "h:nru1R:o:f@:l:cps:b:O:t:XL:", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "h:nNru1R:o:f@:l:cps:b:O:t:XL:", lopts, NULL)) >= 0) {
         switch (c) {
         case 'r': flag |= MERGE_RG; break;
         case 'f': flag |= MERGE_FORCE; break;
         case 'h': fn_headers = optarg; break;
+        case 'N': natural_sort = 0; // fall through
         case 'n': sam_order = QueryName; break;
         case 'o': fnout = optarg; break;
         case 't': sort_tag = optarg; break;
@@ -1926,7 +1944,7 @@ static int bam_merge_simple(SamOrder sam_order, char *sort_tag, const char *out,
     }
 
     if (sam_close(fpout) < 0) {
-        print_error(cmd, "error closing output file");
+        print_error_errno(cmd, "error closing output file \"%s\"", out);
         return -1;
     }
     return 0;
@@ -1961,7 +1979,13 @@ static inline int bam1_cmp_core(const bam1_tag a, const bam1_tag b)
     if (g_sam_order == QueryName || g_sam_order == TagQueryName) {
         int t = strnum_cmp(bam_get_qname(a.bam_record), bam_get_qname(b.bam_record));
         if (t != 0) return t;
-        return (int) (a.bam_record->core.flag&0xc0) - (int) (b.bam_record->core.flag&0xc0);
+        int af = a.bam_record->core.flag;
+        int bf = b.bam_record->core.flag;
+        // Sort order is READ1, READ2, (PRIMARY), SUPPLEMENTARY, SECONDARY
+        // Get the bits in this order so sort is a natural a-b
+        af = ((af&0xc0)<<8)|((af&0x100)<<3)|((af&0x800)>>3);
+        bf = ((bf&0xc0)<<8)|((bf&0x100)<<3)|((bf&0x800)>>3);
+        return af - bf;
     } else {
         pa = a.bam_record->core.tid;
         pb = b.bam_record->core.tid;
@@ -3057,7 +3081,7 @@ static void *worker(void *data)
             break;
         case MinHash:
             worker_minhash(w);
-            // no break, go to merge sort
+            // fall-through
         default:
             ks_mergesort(sort, w->buf_len, w->buf, 0);
     }
@@ -3289,6 +3313,9 @@ int bam_sort_core_ext(SamOrder sam_order, char* sort_tag, int minimiser_kmer,
             break;
         case QueryName:
             new_so = "queryname";
+            new_ss = natural_sort
+                ? "queryname:natural"
+                : "queryname:lexicographical";
             break;
         case MinHash:
             new_so = "coordinate";
@@ -3607,7 +3634,8 @@ static void sort_usage(FILE *fp)
 "  -I FILE    Order minimisers by their position in FILE FASTA\n"
 "  -w INT     Window size for minimiser indexing via -I ref.fa [100]\n"
 "  -H         Squash homopolymers when computing minimiser\n"
-"  -n         Sort by read name (not compatible with samtools index command)\n"
+"  -n         Sort by read name (natural): cannot be used with samtools index\n"
+"  -N         Sort by read name (ASCII): cannot be used with samtools index\n"
 "  -t TAG     Sort by value of TAG. Uses position as secondary index (or read name if -n is set)\n"
 "  -o FILE    Write final output to FILE rather than standard output\n"
 "  -T PREFIX  Write temporary files to PREFIX.nnnn.bam\n"
@@ -3660,9 +3688,10 @@ int bam_sort(int argc, char *argv[])
         { NULL, 0, NULL, 0 }
     };
 
-    while ((c = getopt_long(argc, argv, "l:m:no:O:T:@:t:MI:K:uRw:H", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "l:m:nNo:O:T:@:t:MI:K:uRw:H", lopts, NULL)) >= 0) {
         switch (c) {
         case 'o': fnout = optarg; o_seen = 1; break;
+        case 'N': natural_sort = 0; // fall through
         case 'n': sam_order = QueryName; break;
         case 't': by_tag = true; sort_tag = optarg; break;
         case 'm': {
@@ -3734,7 +3763,7 @@ int bam_sort(int argc, char *argv[])
         goto sort_end;
     }
 
-    if (ga.write_index && (sam_order == QueryName || sam_order == TagQueryName || sam_order == TagCoordinate || sam_order == TemplateCoordinate)) {
+    if (ga.write_index && sam_order != Coordinate) {
         fprintf(samtools_stderr, "[W::bam_sort] Ignoring --write-index as it only works for position sorted files.\n");
         ga.write_index = 0;
     }
index e9f0fb591f8f0b712150d0100431773d7df37949..a12bf7690825c2286f6a920f7abca031cc223694 100644 (file)
@@ -1,6 +1,6 @@
 /*  bam_split.c -- split subcommand.
 
-    Copyright (C) 2013-2016,2018-2019,2023 Genome Research Ltd.
+    Copyright (C) 2013-2016,2018-2019,2023,2024 Genome Research Ltd.
 
     Author: Martin Pollard <mp15@sanger.ac.uk>
 
@@ -48,11 +48,16 @@ struct parsed_opts {
     const char *unaccounted_header_name;
     const char *unaccounted_name;
     const char *output_format_string;
+    const char *tag;
+    long max_split;
     bool verbose;
     int no_pg;
+    int zero_pad;
     sam_global_args ga;
 };
 
+#define DEFAULT_MAX_SPLIT 100
+
 typedef struct parsed_opts parsed_opts_t;
 
 struct state {
@@ -60,13 +65,14 @@ struct state {
     sam_hdr_t* merged_input_header;
     samFile* unaccounted_file;
     sam_hdr_t* unaccounted_header;
+    char *unaccounted_idx_fn;
     size_t output_count;
-    char** rg_id;
-    char **rg_index_file_name;
-    char **rg_output_file_name;
-    samFile** rg_output_file;
-    sam_hdr_t** rg_output_header;
-    kh_c2i_t* rg_hash;
+    char **tag_vals;
+    char **index_file_name;
+    char **output_file_name;
+    samFile **output_file;
+    sam_hdr_t **output_header;
+    kh_c2i_t* tag_val_hash;
     htsThreadPool p;
     int write_index;
 };
@@ -82,19 +88,23 @@ static void usage(FILE *write_to)
 "Usage: samtools split [-u <unaccounted.bam>] [-h <unaccounted_header.sam>]\n"
 "                      [-f <format_string>] [-v] <merged.bam>\n"
 "Options:\n"
-"  -f STRING       output filename format string [\"%%*_%%#.%%.\"]\n"
-"  -u FILE1        put reads with no RG tag or an unrecognised RG tag in FILE1\n"
-"  -h FILE2        ... and override the header with FILE2 (-u file only)\n"
-"  -v              verbose output\n"
-"  --no-PG         do not add a PG line\n");
+"  -f STRING           output filename format string [\"%%*_%%#.%%.\"]\n"
+"  -u FILE1            put left-over reads in FILE1\n"
+"  -h FILE2            ... and override the header with FILE2 (-u file only)\n"
+"  -d TAG              split by TAG value. TAG value must be a string.\n"
+"  -p NUMBER           zero-pad numbers in filenames to NUMBER digits\n"
+"  -M,--max-split NUM  limit number of output files from -d to NUM [%d]\n"
+"  -v                  verbose output\n"
+"  --no-PG             do not add a PG line\n",
+            DEFAULT_MAX_SPLIT);
     sam_global_opt_help(write_to, "-....@..");
     fprintf(write_to,
 "\n"
 "Format string expansions:\n"
 "  %%%%     %%\n"
 "  %%*     basename\n"
-"  %%#     @RG index\n"
-"  %%!     @RG ID\n"
+"  %%#     index (of @RG in the header, or count of TAG values seen so far)\n"
+"  %%!     @RG ID or TAG value\n"
 "  %%.     filename extension for output format\n"
       );
 }
@@ -104,17 +114,21 @@ static parsed_opts_t* parse_args(int argc, char** argv)
 {
     if (argc == 1) { usage(stdout); return NULL; }
 
-    const char *optstring = "vf:h:u:@:";
+    const char *optstring = "vf:h:u:d:M:p:@:";
+    char *default_format_string = "%*_%#.%.";
 
     static const struct option lopts[] = {
         SAM_OPT_GLOBAL_OPTIONS('-', 0, 0, 0, 0, '@'),
         {"no-PG", no_argument, NULL, 1},
+        {"max-split", required_argument, NULL, 'M'},
+        {"zero-pad", required_argument, NULL, 'p'},
         { NULL, 0, NULL, 0 }
     };
 
     parsed_opts_t* retval = calloc(sizeof(parsed_opts_t), 1);
     if (! retval ) { perror("cannot allocate option parsing memory"); return NULL; }
 
+    retval->max_split = DEFAULT_MAX_SPLIT;
     sam_global_args_init(&retval->ga);
 
     int opt;
@@ -132,9 +146,37 @@ static parsed_opts_t* parse_args(int argc, char** argv)
         case 'u':
             retval->unaccounted_name = optarg;
             break;
+        case 'd':
+            retval->tag = optarg;
+            default_format_string = "%*_%!.%.";
+            break;
+        case 'M': {
+            char *end = optarg;
+            retval->max_split = strtol(optarg, &end, 10);
+            if (*optarg == '\0' || *end != '\0' || retval->max_split == 0) {
+                print_error("split", "Invalid -M argument: \"%s\"", optarg);
+                free(retval);
+                return NULL;
+            }
+            if (retval->max_split < 0) // No limit requested
+                retval->max_split = LONG_MAX;
+            break;
+        }
+        case 'p': {
+            char *end = optarg;
+            unsigned long val = strtoul(optarg, &end, 10);
+            if (*optarg == '\0' || *end != '\0' || val > 20) {
+                print_error("split", "Invalid -p argument: \"%s\"", optarg);
+                free(retval);
+                return NULL;
+            }
+            retval->zero_pad = (int) val;
+            break;
+        }
         case 1:
             retval->no_pg = 1;
             break;
+
         default:
             if (parse_sam_global_opt(opt, optarg, lopts, &retval->ga) == 0) break;
             /* else fall-through */
@@ -145,7 +187,7 @@ static parsed_opts_t* parse_args(int argc, char** argv)
         }
     }
 
-    if (retval->output_format_string == NULL) retval->output_format_string = "%*_%#.%.";
+    if (retval->output_format_string == NULL) retval->output_format_string = default_format_string;
 
     argc -= optind;
     argv += optind;
@@ -163,7 +205,10 @@ static parsed_opts_t* parse_args(int argc, char** argv)
 }
 
 // Expands a output filename format string
-static char* expand_format_string(const char* format_string, const char* basename, const char* rg_id, const int rg_idx, const htsFormat *format)
+static char* expand_format_string(const char* format_string,
+                                  const char* basename, const char* tag_val,
+                                  const int file_idx, const int zero_pad,
+                                  const htsFormat *format)
 {
     kstring_t str = { 0, 0, NULL };
     const char* pointer = format_string;
@@ -179,10 +224,15 @@ static char* expand_format_string(const char* format_string, const char* basenam
                 if (kputs(basename, &str) < 0) goto memfail;
                 break;
             case '#':
-                if (kputl(rg_idx, &str) < 0) goto memfail;
+                if (zero_pad == 0) {
+                    if (kputl(file_idx, &str) < 0) goto memfail;
+                } else {
+                    if (ksprintf(&str, "%0*d", zero_pad, file_idx) < 0)
+                        goto memfail;
+                }
                 break;
             case '!':
-                if (kputs(rg_id, &str) < 0) goto memfail;
+                if (kputs(tag_val, &str) < 0) goto memfail;
                 break;
             case '.':
                 // Only really need to cope with sam, bam, cram
@@ -275,6 +325,157 @@ static int header_compatible(sam_hdr_t *hdr1, sam_hdr_t *hdr2)
     return 0;
 }
 
+static int grow_output_lists(state_t *state, size_t count) {
+    char **new_list = realloc(state->tag_vals, count * sizeof(char *));
+    if (!new_list)
+        return -1;
+    state->tag_vals = new_list;
+    new_list = realloc(state->index_file_name, count * sizeof(char *));
+    if (!new_list)
+        return -1;
+    state->index_file_name = new_list;
+    new_list = realloc(state->output_file_name, count * sizeof(char *));
+    if (!new_list)
+        return -1;
+    state->output_file_name = new_list;
+    samFile **new_file = realloc(state->output_file,
+                                 count * sizeof(samFile *));
+    if (!new_file)
+        return -1;
+    state->output_file = new_file;
+    sam_hdr_t **new_hdr = realloc(state->output_header,
+                                  count * sizeof(sam_hdr_t *));
+    if (!new_hdr)
+        return -1;
+    state->output_header = new_hdr;
+    return 0;
+}
+
+static khiter_t prep_sam_file(parsed_opts_t *opts, state_t *state,
+                              const char *tag, const char *arg_list,
+                              int is_rg) {
+    char *input_base_name = NULL, *new_file_name = NULL, *tag_val = NULL;
+    char *new_idx_fn = NULL;
+    sam_hdr_t *new_hdr = NULL;
+    samFile *new_sam_file = NULL;
+
+    khiter_t i = kh_get_c2i(state->tag_val_hash, tag);
+    if (i != kh_end(state->tag_val_hash)) {
+        return i;
+    }
+    // create new file
+    if (grow_output_lists(state, state->output_count + 1) != 0) {
+        print_error_errno("split", "Couldn't grow output lists");
+        return kh_end(state->tag_val_hash);
+    }
+    tag_val = strdup(tag);
+    if (!tag_val) {
+        print_error_errno("split", "Couldn't copy tag value");
+        return kh_end(state->tag_val_hash);
+    }
+    char *dirsep = strrchr(opts->merged_input_name, '/');
+    input_base_name = strdup(dirsep? dirsep+1 : opts->merged_input_name);
+    if (!input_base_name) {
+        print_error_errno("split", "Filename parsing failed");
+        goto fail;
+    }
+
+    char* extension = strrchr(input_base_name, '.');
+    if (extension) *extension = '\0';
+
+    new_file_name = expand_format_string(opts->output_format_string,
+                                         input_base_name, tag,
+                                         kh_size(state->tag_val_hash),
+                                         opts->zero_pad, &opts->ga.out);
+    if (!new_file_name) {
+        print_error_errno("split", "Filename creation failed");
+        goto fail;
+    }
+
+    new_hdr = sam_hdr_dup(state->merged_input_header);
+    if (!new_hdr) {
+        print_error_errno("split", "Duplicating header for file \"%s\" failed", new_file_name);
+        goto fail;
+    }
+    if (!opts->no_pg && sam_hdr_add_pg(new_hdr, "samtools",
+                                       "VN", samtools_version(),
+                                       arg_list ? "CL": NULL,
+                                       arg_list ? arg_list : NULL,
+                                       NULL)) {
+        print_error_errno("split", "Adding PG line to file \"%s\" failed", new_file_name);
+        goto fail;
+    }
+
+    if (is_rg) {
+        // If here, we've found an RG:Z: tag without a corresponding @RG
+        // line in the header.
+        if (sam_hdr_remove_lines(new_hdr, "RG", "ID", NULL) != 0) {
+            print_error_errno("split",
+                              "Failed to remove @RG lines from file \"%s\"",
+                              new_file_name);
+            goto fail;
+        }
+        if (sam_hdr_add_line(new_hdr, "RG", "ID", tag_val, NULL) != 0) {
+            print_error_errno("split",
+                              "Failed to add @RG line to file \"%s\"",
+                              new_file_name);
+            goto fail;
+        }
+    }
+
+    char outmode[4] = "w";
+    sam_open_mode(outmode + 1, new_file_name, NULL);
+    new_sam_file = sam_open_format(new_file_name, outmode, &opts->ga.out);
+    if (!new_sam_file) {
+        print_error_errno("split", "Opening filename for writing \"%s\" failed", new_file_name);
+        goto fail;
+    }
+    if (state->p.pool)
+        hts_set_opt(new_sam_file, HTS_OPT_THREAD_POOL, &state->p);
+
+    if (sam_hdr_write(new_sam_file, new_hdr) != 0) {
+        print_error_errno("split", "Couldn't write header to \"%s\"",
+                          new_file_name);
+        goto fail;
+    }
+
+    if (state->write_index) {
+        new_idx_fn = auto_index(new_sam_file, new_file_name, new_hdr);
+        if (!new_idx_fn) {
+            print_error_errno("split", "Creating index file for file \"%s\" failed", new_file_name);
+            goto fail;
+        }
+    }
+
+    int ret = -1;
+    i = kh_put_c2i(state->tag_val_hash, tag_val, &ret);
+    if (ret < 0) {
+        print_error_errno("split", "Adding file \"%s\" failed", new_file_name);
+        goto fail;
+    }
+
+    kh_val(state->tag_val_hash, i) = state->output_count;
+    state->tag_vals[state->output_count] = tag_val;
+    state->index_file_name[state->output_count] = new_idx_fn;
+    state->output_file_name[state->output_count] = new_file_name;
+    state->output_file[state->output_count] = new_sam_file;
+    state->output_header[state->output_count] = new_hdr;
+    state->output_count++;
+    free(input_base_name);
+    return i;
+
+ fail:
+    free(input_base_name);
+    free(new_file_name);
+    free(tag_val);
+    free(new_idx_fn);
+    if (new_hdr)
+        sam_hdr_destroy(new_hdr);
+    if (new_sam_file)
+        sam_close(new_sam_file);
+    return kh_end(state->tag_val_hash);
+}
+
 // Set the initial state
 static state_t* init(parsed_opts_t* opts, const char *arg_list)
 {
@@ -306,6 +507,7 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
         cleanup_state(retval, false);
         return NULL;
     }
+    retval->write_index = opts->ga.write_index;
 
     if (opts->unaccounted_name) {
         if (opts->unaccounted_header_name) {
@@ -331,10 +533,10 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
         } else {
             retval->unaccounted_header = sam_hdr_dup(retval->merged_input_header);
             if (!opts->no_pg && sam_hdr_add_pg(retval->unaccounted_header, "samtools",
-                                    "VN", samtools_version(),
-                                    arg_list ? "CL": NULL,
-                                    arg_list ? arg_list : NULL,
-                                    NULL)) {
+                                               "VN", samtools_version(),
+                                               arg_list ? "CL": NULL,
+                                               arg_list ? arg_list : NULL,
+                                               NULL)) {
                 print_error("split", "Could not rewrite header for \"%s\"", opts->unaccounted_name);
                 cleanup_state(retval, false);
                 return NULL;
@@ -354,23 +556,40 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
             hts_set_opt(retval->unaccounted_file, HTS_OPT_THREAD_POOL, &retval->p);
     }
 
-    // Open output files for RGs
-    if (!count_RG(retval->merged_input_header, &retval->output_count, &retval->rg_id)) return NULL;
-    if (opts->verbose) fprintf(stderr, "@RG's found %zu\n",retval->output_count);
+    int is_rg = !opts->tag || strcmp(opts->tag, "RG") == 0;
+    if (is_rg) {
+        if (!count_RG(retval->merged_input_header,
+                      &retval->output_count, &retval->tag_vals)) {
+            cleanup_state(retval, false);
+            return NULL;
+        }
+        if (opts->verbose)
+            fprintf(stderr, "@RG's found %zu\n",retval->output_count);
+    } else {
+        retval->output_count = 0;
+    }
+
     // Prevent calloc(0, size);
     size_t num = retval->output_count ? retval->output_count : 1;
-    retval->rg_index_file_name = (char **)calloc(num, sizeof(char *));
-    retval->rg_output_file_name = (char **)calloc(num, sizeof(char *));
-    retval->rg_output_file = (samFile**)calloc(num, sizeof(samFile*));
-    retval->rg_output_header = (sam_hdr_t**)calloc(num, sizeof(sam_hdr_t*));
-    retval->rg_hash = kh_init_c2i();
-    if (!retval->rg_output_file_name || !retval->rg_output_file || !retval->rg_output_header ||
-        !retval->rg_hash || !retval->rg_index_file_name) {
+    retval->index_file_name = (char **)calloc(num, sizeof(char *));
+    retval->output_file_name = (char **)calloc(num, sizeof(char *));
+    retval->output_file = (samFile**)calloc(num, sizeof(samFile*));
+    retval->output_header = (sam_hdr_t**)calloc(num, sizeof(sam_hdr_t*));
+    retval->tag_val_hash = kh_init_c2i();
+    if (!retval->output_file_name || !retval->output_file || !retval->output_header ||
+        !retval->tag_val_hash || !retval->index_file_name) {
         print_error_errno("split", "Could not initialise output file array");
         cleanup_state(retval, false);
         return NULL;
     }
+    if (!is_rg)
+        return retval;  // Done for this case - outputs will be opened later
 
+    // Adjust max_split if too small for the read-groups listed in the header
+    if (opts->max_split < retval->output_count)
+        opts->max_split = retval->output_count;
+
+    // Open output files for RGs
     char* dirsep = strrchr(opts->merged_input_name, '/');
     char* input_base_name = strdup(dirsep? dirsep+1 : opts->merged_input_name);
     if (!input_base_name) {
@@ -388,8 +607,8 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
 
         output_filename = expand_format_string(opts->output_format_string,
                                                input_base_name,
-                                               retval->rg_id[i], i,
-                                               &opts->ga.out);
+                                               retval->tag_vals[i], i,
+                                               opts->zero_pad, &opts->ga.out);
 
         if ( output_filename == NULL ) {
             cleanup_state(retval, false);
@@ -397,40 +616,39 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
             return NULL;
         }
 
-        retval->rg_output_file_name[i] = output_filename;
-
+        retval->output_file_name[i] = output_filename;
         sam_open_mode(outmode + 1, output_filename, NULL);
-        retval->rg_output_file[i] = sam_open_format(output_filename, outmode, &opts->ga.out);
+        retval->output_file[i] = sam_open_format(output_filename, outmode, &opts->ga.out);
 
-        if (retval->rg_output_file[i] == NULL) {
+        if (retval->output_file[i] == NULL) {
             print_error_errno("split", "Could not open \"%s\"", output_filename);
             cleanup_state(retval, false);
             free(input_base_name);
             return NULL;
         }
         if (retval->p.pool)
-            hts_set_opt(retval->rg_output_file[i], HTS_OPT_THREAD_POOL, &retval->p);
+            hts_set_opt(retval->output_file[i], HTS_OPT_THREAD_POOL, &retval->p);
 
         // Record index in hash
         int ret;
-        khiter_t iter = kh_put_c2i(retval->rg_hash, retval->rg_id[i], &ret);
+        khiter_t iter = kh_put_c2i(retval->tag_val_hash, retval->tag_vals[i], &ret);
         if (ret < 0) {
             print_error_errno("split", "Couldn't add @RG ID to look-up table");
             cleanup_state(retval, false);
             free(input_base_name);
             return NULL;
         }
-        kh_val(retval->rg_hash,iter) = i;
+        kh_val(retval->tag_val_hash,iter) = i;
 
         // Set and edit header
-        retval->rg_output_header[i] = sam_hdr_dup(retval->merged_input_header);
-        if (sam_hdr_remove_except(retval->rg_output_header[i], "RG", "ID", retval->rg_id[i]) ||
-           (!opts->no_pg &&
-            sam_hdr_add_pg(retval->rg_output_header[i], "samtools",
-                        "VN", samtools_version(),
-                        arg_list ? "CL": NULL,
-                        arg_list ? arg_list : NULL,
-                        NULL))) {
+        retval->output_header[i] = sam_hdr_dup(retval->merged_input_header);
+        if (sam_hdr_remove_except(retval->output_header[i], "RG", "ID", retval->tag_vals[i]) ||
+            (!opts->no_pg &&
+             sam_hdr_add_pg(retval->output_header[i], "samtools",
+                            "VN", samtools_version(),
+                            arg_list ? "CL": NULL,
+                            arg_list ? arg_list : NULL,
+                            NULL))) {
             print_error("split", "Could not rewrite header for \"%s\"", output_filename);
             cleanup_state(retval, false);
             free(input_base_name);
@@ -439,34 +657,30 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
     }
 
     free(input_base_name);
-    retval->write_index = opts->ga.write_index;
 
     return retval;
 }
 
-static bool split(state_t* state)
+static bool split(state_t* state, parsed_opts_t *opts, char *arg_list)
 {
-    if (state->unaccounted_file && sam_hdr_write(state->unaccounted_file, state->unaccounted_header) != 0) {
-        print_error_errno("split", "Could not write output file header");
-        return false;
-    }
-    size_t i;
-    for (i = 0; i < state->output_count; i++) {
-        if (sam_hdr_write(state->rg_output_file[i], state->rg_output_header[i]) != 0) {
-            print_error_errno("split", "Could not write file header to \"%s\"", state->rg_output_file_name[i]);
+    int is_rg = !opts->tag || strcmp(opts->tag, "RG") == 0;
+    if (state->unaccounted_file) {
+        if (sam_hdr_write(state->unaccounted_file, state->unaccounted_header) != 0) {
+            print_error_errno("split", "Could not write output file header");
             return false;
         }
-        if (state->write_index) {
-            state->rg_index_file_name[i] = auto_index(state->rg_output_file[i],
-                                                      state->rg_output_file_name[i],
-                                                      state->rg_output_header[i]);
-            if (!state->rg_index_file_name[i]) {
-                print_error_errno("split", "Could not create index for file \"%s\"", state->rg_output_file_name[i]);
+        if (opts->ga.write_index) {
+            state->unaccounted_idx_fn = auto_index(state->unaccounted_file,
+                                                   opts->unaccounted_name,
+                                                   state->unaccounted_header);
+            if (!state->unaccounted_idx_fn) {
+                print_error_errno("split",
+                                  "Creating index file for file \"%s\" failed",
+                                  opts->unaccounted_name);
                 return false;
             }
         }
     }
-
     bam1_t* file_read = bam_init1();
     // Read the first record
     int r;
@@ -480,25 +694,79 @@ static bool split(state_t* state)
         }
     }
 
+    if (is_rg) {
+        size_t i;
+        for (i = 0; i < state->output_count; i++) {
+            if (sam_hdr_write(state->output_file[i], state->output_header[i]) != 0) {
+                print_error_errno("split", "Could not write file header to \"%s\"", state->output_file_name[i]);
+                goto error;
+            }
+            if (state->write_index) {
+                state->index_file_name[i] = auto_index(state->output_file[i],
+                        state->output_file_name[i],
+                        state->output_header[i]);
+                if (!state->index_file_name[i]) {
+                    print_error_errno("split", "Could not create index for file \"%s\"", state->output_file_name[i]);
+                    goto error;
+                }
+            }
+        }
+    }
     while (file_read != NULL) {
         // Get RG tag from read and look it up in hash to find file to output it to
-        uint8_t* tag = bam_aux_get(file_read, "RG");
+        uint8_t* tag = bam_aux_get(file_read, is_rg ? "RG" : opts->tag);
+        char *val = NULL;
+        char number[28];
         khiter_t iter;
-        if ( tag != NULL ) {
-            char* rg = bam_aux2Z(tag);
-            iter = kh_get_c2i(state->rg_hash, rg);
+        if (tag) {
+            switch (*tag) {
+            case 'Z': case 'H':
+                val = bam_aux2Z(tag);
+                break;
+            case 'c': case 'C': case 's': case 'S': case 'i': case 'I':
+                if (opts->zero_pad == 0) {
+                    snprintf(number, sizeof(number), "%"PRId64, bam_aux2i(tag));
+                } else {
+                    int64_t v = bam_aux2i(tag);
+                    snprintf(number, sizeof(number), "%0*"PRId64,
+                             v < 0 ? opts->zero_pad + 1 : opts->zero_pad, v);
+                }
+                val = number;
+                break;
+            default:
+                break;
+            }
+        }
+        if ( val != NULL ) {
+            iter = kh_get_c2i(state->tag_val_hash, val);
         } else {
-            iter = kh_end(state->rg_hash);
+            iter = kh_end(state->tag_val_hash);
+        }
+
+        // Check for opts->tag here instead of !is_rg so we open new
+        // files if the user specified '-d RG' and we find a RG:Z: value
+        // that wasn't listed in the header.  If the '-d' option is
+        // not used, we don't open a file to preserve existing behaviour.
+        if (opts->tag && val && iter == kh_end(state->tag_val_hash)
+            && state->output_count < opts->max_split) {
+            // Need to open a new output file
+            iter = prep_sam_file(opts, state, val, arg_list, is_rg);
+            if (iter == kh_end(state->tag_val_hash)) { // Open failed
+                print_error("split",
+                            "Could not create output file for tag \"%s:%s\"",
+                            opts->tag, bam_aux2Z(tag));
+                goto error;
+
+            }
         }
 
         // Write the read out to correct file
-        if (iter != kh_end(state->rg_hash)) {
+        if (iter != kh_end(state->tag_val_hash)) {
             // if found write to the appropriate untangled bam
-            int i = kh_val(state->rg_hash,iter);
-            if (sam_write1(state->rg_output_file[i], state->rg_output_header[i], file_read) < 0) {
-                print_error_errno("split", "Could not write to \"%s\"", state->rg_output_file_name[i]);
-                bam_destroy1(file_read);
-                return false;
+            int i = kh_val(state->tag_val_hash,iter);
+            if (sam_write1(state->output_file[i], state->output_header[i], file_read) < 0) {
+                print_error_errno("split", "Could not write to \"%s\"", state->output_file_name[i]);
+                goto error;
             }
         } else {
             // otherwise write to the unaccounted bam if there is one or fail
@@ -506,15 +774,14 @@ static bool split(state_t* state)
                 if (tag) {
                     fprintf(stderr, "Read \"%s\" with unaccounted for tag \"%s\".\n", bam_get_qname(file_read), bam_aux2Z(tag));
                 } else {
-                    fprintf(stderr, "Read \"%s\" has no RG tag.\n", bam_get_qname(file_read));
+                    fprintf(stderr, "Read \"%s\" has no %s tag.\n",
+                            bam_get_qname(file_read), is_rg ? "RG" : opts->tag);
                 }
-                bam_destroy1(file_read);
-                return false;
+                goto error;
             } else {
                 if (sam_write1(state->unaccounted_file, state->unaccounted_header, file_read) < 0) {
                     print_error_errno("split", "Could not write to unaccounted output file");
-                    bam_destroy1(file_read);
-                    return false;
+                    goto error;
                 }
             }
         }
@@ -532,16 +799,27 @@ static bool split(state_t* state)
     }
 
     if (state->write_index) {
+        size_t i;
         for (i = 0; i < state->output_count; i++) {
-            if (sam_idx_save(state->rg_output_file[i]) < 0) {
-                print_error_errno("split", "writing index failed");
+            if (sam_idx_save(state->output_file[i]) < 0) {
+                print_error_errno("split", "writing index \"%s\" failed",
+                                  state->index_file_name[i]);
+                return false;
+            }
+        }
+        if (state->unaccounted_file) {
+            if (sam_idx_save(state->unaccounted_file) < 0) {
+                print_error_errno("split", "writing index \"%s\" failed",
+                                  state->unaccounted_idx_fn);
                 return false;
             }
-            free(state->rg_index_file_name[i]);
         }
     }
 
     return true;
+error:
+    bam_destroy1(file_read);
+    return false;
 }
 
 static int cleanup_state(state_t* status, bool check_close)
@@ -559,25 +837,28 @@ static int cleanup_state(state_t* status, bool check_close)
     sam_close(status->merged_input_file);
     size_t i;
     for (i = 0; i < status->output_count; i++) {
-        if (status->rg_output_header && status->rg_output_header[i])
-            sam_hdr_destroy(status->rg_output_header[i]);
-        if (status->rg_output_file && status->rg_output_file[i]) {
-            if (sam_close(status->rg_output_file[i]) < 0 && check_close) {
-                print_error("split", "Error on closing output file \"%s\"", status->rg_output_file_name[i]);
+        if (status->output_header && status->output_header[i])
+            sam_hdr_destroy(status->output_header[i]);
+        if (status->output_file && status->output_file[i]) {
+            if (sam_close(status->output_file[i]) < 0 && check_close) {
+                print_error("split", "Error on closing output file \"%s\"", status->output_file_name[i]);
                 ret = -1;
             }
         }
-        if (status->rg_id) free(status->rg_id[i]);
-        if (status->rg_output_file_name) free(status->rg_output_file_name[i]);
+        if (status->tag_vals) free(status->tag_vals[i]);
+        if (status->output_file_name) free(status->output_file_name[i]);
+        if (status->index_file_name[i]) free(status->index_file_name[i]);
     }
     if (status->merged_input_header)
         sam_hdr_destroy(status->merged_input_header);
-    free(status->rg_output_header);
-    free(status->rg_output_file);
-    free(status->rg_output_file_name);
-    free(status->rg_index_file_name);
-    kh_destroy_c2i(status->rg_hash);
-    free(status->rg_id);
+    free(status->output_header);
+    free(status->output_file);
+    free(status->output_file_name);
+    free(status->index_file_name);
+    free(status->unaccounted_idx_fn);
+    kh_destroy_c2i(status->tag_val_hash);
+
+    free(status->tag_vals);
     if (status->p.pool)
         hts_tpool_destroy(status->p.pool);
     free(status);
@@ -603,7 +884,7 @@ int main_split(int argc, char** argv)
     state_t* status = init(opts, arg_list);
     if (!status) goto cleanup_opts;
 
-    if (!split(status)) {
+    if (!split(status, opts, arg_list)) {
         cleanup_state(status, false);
         goto cleanup_opts;
     }
index 6c48466da729b35e22850fec99c935b57a56b389..f1fd5f4d85ab7dfd3d145818e874d076e23b72af 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  bam_split.c -- split subcommand.
 
-    Copyright (C) 2013-2016,2018-2019,2023 Genome Research Ltd.
+    Copyright (C) 2013-2016,2018-2019,2023,2024 Genome Research Ltd.
 
     Author: Martin Pollard <mp15@sanger.ac.uk>
 
@@ -50,11 +50,16 @@ struct parsed_opts {
     const char *unaccounted_header_name;
     const char *unaccounted_name;
     const char *output_format_string;
+    const char *tag;
+    long max_split;
     bool verbose;
     int no_pg;
+    int zero_pad;
     sam_global_args ga;
 };
 
+#define DEFAULT_MAX_SPLIT 100
+
 typedef struct parsed_opts parsed_opts_t;
 
 struct state {
@@ -62,13 +67,14 @@ struct state {
     sam_hdr_t* merged_input_header;
     samFile* unaccounted_file;
     sam_hdr_t* unaccounted_header;
+    char *unaccounted_idx_fn;
     size_t output_count;
-    char** rg_id;
-    char **rg_index_file_name;
-    char **rg_output_file_name;
-    samFile** rg_output_file;
-    sam_hdr_t** rg_output_header;
-    kh_c2i_t* rg_hash;
+    char **tag_vals;
+    char **index_file_name;
+    char **output_file_name;
+    samFile **output_file;
+    sam_hdr_t **output_header;
+    kh_c2i_t* tag_val_hash;
     htsThreadPool p;
     int write_index;
 };
@@ -84,19 +90,23 @@ static void usage(FILE *write_to)
 "Usage: samtools split [-u <unaccounted.bam>] [-h <unaccounted_header.sam>]\n"
 "                      [-f <format_string>] [-v] <merged.bam>\n"
 "Options:\n"
-"  -f STRING       output filename format string [\"%%*_%%#.%%.\"]\n"
-"  -u FILE1        put reads with no RG tag or an unrecognised RG tag in FILE1\n"
-"  -h FILE2        ... and override the header with FILE2 (-u file only)\n"
-"  -v              verbose output\n"
-"  --no-PG         do not add a PG line\n");
+"  -f STRING           output filename format string [\"%%*_%%#.%%.\"]\n"
+"  -u FILE1            put left-over reads in FILE1\n"
+"  -h FILE2            ... and override the header with FILE2 (-u file only)\n"
+"  -d TAG              split by TAG value. TAG value must be a string.\n"
+"  -p NUMBER           zero-pad numbers in filenames to NUMBER digits\n"
+"  -M,--max-split NUM  limit number of output files from -d to NUM [%d]\n"
+"  -v                  verbose output\n"
+"  --no-PG             do not add a PG line\n",
+            DEFAULT_MAX_SPLIT);
     sam_global_opt_help(write_to, "-....@..");
     fprintf(write_to,
 "\n"
 "Format string expansions:\n"
 "  %%%%     %%\n"
 "  %%*     basename\n"
-"  %%#     @RG index\n"
-"  %%!     @RG ID\n"
+"  %%#     index (of @RG in the header, or count of TAG values seen so far)\n"
+"  %%!     @RG ID or TAG value\n"
 "  %%.     filename extension for output format\n"
       );
 }
@@ -106,17 +116,21 @@ static parsed_opts_t* parse_args(int argc, char** argv)
 {
     if (argc == 1) { usage(samtools_stdout); return NULL; }
 
-    const char *optstring = "vf:h:u:@:";
+    const char *optstring = "vf:h:u:d:M:p:@:";
+    char *default_format_string = "%*_%#.%.";
 
     static const struct option lopts[] = {
         SAM_OPT_GLOBAL_OPTIONS('-', 0, 0, 0, 0, '@'),
         {"no-PG", no_argument, NULL, 1},
+        {"max-split", required_argument, NULL, 'M'},
+        {"zero-pad", required_argument, NULL, 'p'},
         { NULL, 0, NULL, 0 }
     };
 
     parsed_opts_t* retval = calloc(sizeof(parsed_opts_t), 1);
     if (! retval ) { perror("cannot allocate option parsing memory"); return NULL; }
 
+    retval->max_split = DEFAULT_MAX_SPLIT;
     sam_global_args_init(&retval->ga);
 
     int opt;
@@ -134,9 +148,37 @@ static parsed_opts_t* parse_args(int argc, char** argv)
         case 'u':
             retval->unaccounted_name = optarg;
             break;
+        case 'd':
+            retval->tag = optarg;
+            default_format_string = "%*_%!.%.";
+            break;
+        case 'M': {
+            char *end = optarg;
+            retval->max_split = strtol(optarg, &end, 10);
+            if (*optarg == '\0' || *end != '\0' || retval->max_split == 0) {
+                print_error("split", "Invalid -M argument: \"%s\"", optarg);
+                free(retval);
+                return NULL;
+            }
+            if (retval->max_split < 0) // No limit requested
+                retval->max_split = LONG_MAX;
+            break;
+        }
+        case 'p': {
+            char *end = optarg;
+            unsigned long val = strtoul(optarg, &end, 10);
+            if (*optarg == '\0' || *end != '\0' || val > 20) {
+                print_error("split", "Invalid -p argument: \"%s\"", optarg);
+                free(retval);
+                return NULL;
+            }
+            retval->zero_pad = (int) val;
+            break;
+        }
         case 1:
             retval->no_pg = 1;
             break;
+
         default:
             if (parse_sam_global_opt(opt, optarg, lopts, &retval->ga) == 0) break;
             /* else fall-through */
@@ -147,7 +189,7 @@ static parsed_opts_t* parse_args(int argc, char** argv)
         }
     }
 
-    if (retval->output_format_string == NULL) retval->output_format_string = "%*_%#.%.";
+    if (retval->output_format_string == NULL) retval->output_format_string = default_format_string;
 
     argc -= optind;
     argv += optind;
@@ -165,7 +207,10 @@ static parsed_opts_t* parse_args(int argc, char** argv)
 }
 
 // Expands a output filename format string
-static char* expand_format_string(const char* format_string, const char* basename, const char* rg_id, const int rg_idx, const htsFormat *format)
+static char* expand_format_string(const char* format_string,
+                                  const char* basename, const char* tag_val,
+                                  const int file_idx, const int zero_pad,
+                                  const htsFormat *format)
 {
     kstring_t str = { 0, 0, NULL };
     const char* pointer = format_string;
@@ -181,10 +226,15 @@ static char* expand_format_string(const char* format_string, const char* basenam
                 if (kputs(basename, &str) < 0) goto memfail;
                 break;
             case '#':
-                if (kputl(rg_idx, &str) < 0) goto memfail;
+                if (zero_pad == 0) {
+                    if (kputl(file_idx, &str) < 0) goto memfail;
+                } else {
+                    if (ksprintf(&str, "%0*d", zero_pad, file_idx) < 0)
+                        goto memfail;
+                }
                 break;
             case '!':
-                if (kputs(rg_id, &str) < 0) goto memfail;
+                if (kputs(tag_val, &str) < 0) goto memfail;
                 break;
             case '.':
                 // Only really need to cope with sam, bam, cram
@@ -277,6 +327,157 @@ static int header_compatible(sam_hdr_t *hdr1, sam_hdr_t *hdr2)
     return 0;
 }
 
+static int grow_output_lists(state_t *state, size_t count) {
+    char **new_list = realloc(state->tag_vals, count * sizeof(char *));
+    if (!new_list)
+        return -1;
+    state->tag_vals = new_list;
+    new_list = realloc(state->index_file_name, count * sizeof(char *));
+    if (!new_list)
+        return -1;
+    state->index_file_name = new_list;
+    new_list = realloc(state->output_file_name, count * sizeof(char *));
+    if (!new_list)
+        return -1;
+    state->output_file_name = new_list;
+    samFile **new_file = realloc(state->output_file,
+                                 count * sizeof(samFile *));
+    if (!new_file)
+        return -1;
+    state->output_file = new_file;
+    sam_hdr_t **new_hdr = realloc(state->output_header,
+                                  count * sizeof(sam_hdr_t *));
+    if (!new_hdr)
+        return -1;
+    state->output_header = new_hdr;
+    return 0;
+}
+
+static khiter_t prep_sam_file(parsed_opts_t *opts, state_t *state,
+                              const char *tag, const char *arg_list,
+                              int is_rg) {
+    char *input_base_name = NULL, *new_file_name = NULL, *tag_val = NULL;
+    char *new_idx_fn = NULL;
+    sam_hdr_t *new_hdr = NULL;
+    samFile *new_sam_file = NULL;
+
+    khiter_t i = kh_get_c2i(state->tag_val_hash, tag);
+    if (i != kh_end(state->tag_val_hash)) {
+        return i;
+    }
+    // create new file
+    if (grow_output_lists(state, state->output_count + 1) != 0) {
+        print_error_errno("split", "Couldn't grow output lists");
+        return kh_end(state->tag_val_hash);
+    }
+    tag_val = strdup(tag);
+    if (!tag_val) {
+        print_error_errno("split", "Couldn't copy tag value");
+        return kh_end(state->tag_val_hash);
+    }
+    char *dirsep = strrchr(opts->merged_input_name, '/');
+    input_base_name = strdup(dirsep? dirsep+1 : opts->merged_input_name);
+    if (!input_base_name) {
+        print_error_errno("split", "Filename parsing failed");
+        goto fail;
+    }
+
+    char* extension = strrchr(input_base_name, '.');
+    if (extension) *extension = '\0';
+
+    new_file_name = expand_format_string(opts->output_format_string,
+                                         input_base_name, tag,
+                                         kh_size(state->tag_val_hash),
+                                         opts->zero_pad, &opts->ga.out);
+    if (!new_file_name) {
+        print_error_errno("split", "Filename creation failed");
+        goto fail;
+    }
+
+    new_hdr = sam_hdr_dup(state->merged_input_header);
+    if (!new_hdr) {
+        print_error_errno("split", "Duplicating header for file \"%s\" failed", new_file_name);
+        goto fail;
+    }
+    if (!opts->no_pg && sam_hdr_add_pg(new_hdr, "samtools",
+                                       "VN", samtools_version(),
+                                       arg_list ? "CL": NULL,
+                                       arg_list ? arg_list : NULL,
+                                       NULL)) {
+        print_error_errno("split", "Adding PG line to file \"%s\" failed", new_file_name);
+        goto fail;
+    }
+
+    if (is_rg) {
+        // If here, we've found an RG:Z: tag without a corresponding @RG
+        // line in the header.
+        if (sam_hdr_remove_lines(new_hdr, "RG", "ID", NULL) != 0) {
+            print_error_errno("split",
+                              "Failed to remove @RG lines from file \"%s\"",
+                              new_file_name);
+            goto fail;
+        }
+        if (sam_hdr_add_line(new_hdr, "RG", "ID", tag_val, NULL) != 0) {
+            print_error_errno("split",
+                              "Failed to add @RG line to file \"%s\"",
+                              new_file_name);
+            goto fail;
+        }
+    }
+
+    char outmode[4] = "w";
+    sam_open_mode(outmode + 1, new_file_name, NULL);
+    new_sam_file = sam_open_format(new_file_name, outmode, &opts->ga.out);
+    if (!new_sam_file) {
+        print_error_errno("split", "Opening filename for writing \"%s\" failed", new_file_name);
+        goto fail;
+    }
+    if (state->p.pool)
+        hts_set_opt(new_sam_file, HTS_OPT_THREAD_POOL, &state->p);
+
+    if (sam_hdr_write(new_sam_file, new_hdr) != 0) {
+        print_error_errno("split", "Couldn't write header to \"%s\"",
+                          new_file_name);
+        goto fail;
+    }
+
+    if (state->write_index) {
+        new_idx_fn = auto_index(new_sam_file, new_file_name, new_hdr);
+        if (!new_idx_fn) {
+            print_error_errno("split", "Creating index file for file \"%s\" failed", new_file_name);
+            goto fail;
+        }
+    }
+
+    int ret = -1;
+    i = kh_put_c2i(state->tag_val_hash, tag_val, &ret);
+    if (ret < 0) {
+        print_error_errno("split", "Adding file \"%s\" failed", new_file_name);
+        goto fail;
+    }
+
+    kh_val(state->tag_val_hash, i) = state->output_count;
+    state->tag_vals[state->output_count] = tag_val;
+    state->index_file_name[state->output_count] = new_idx_fn;
+    state->output_file_name[state->output_count] = new_file_name;
+    state->output_file[state->output_count] = new_sam_file;
+    state->output_header[state->output_count] = new_hdr;
+    state->output_count++;
+    free(input_base_name);
+    return i;
+
+ fail:
+    free(input_base_name);
+    free(new_file_name);
+    free(tag_val);
+    free(new_idx_fn);
+    if (new_hdr)
+        sam_hdr_destroy(new_hdr);
+    if (new_sam_file)
+        sam_close(new_sam_file);
+    return kh_end(state->tag_val_hash);
+}
+
 // Set the initial state
 static state_t* init(parsed_opts_t* opts, const char *arg_list)
 {
@@ -308,6 +509,7 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
         cleanup_state(retval, false);
         return NULL;
     }
+    retval->write_index = opts->ga.write_index;
 
     if (opts->unaccounted_name) {
         if (opts->unaccounted_header_name) {
@@ -333,10 +535,10 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
         } else {
             retval->unaccounted_header = sam_hdr_dup(retval->merged_input_header);
             if (!opts->no_pg && sam_hdr_add_pg(retval->unaccounted_header, "samtools",
-                                    "VN", samtools_version(),
-                                    arg_list ? "CL": NULL,
-                                    arg_list ? arg_list : NULL,
-                                    NULL)) {
+                                               "VN", samtools_version(),
+                                               arg_list ? "CL": NULL,
+                                               arg_list ? arg_list : NULL,
+                                               NULL)) {
                 print_error("split", "Could not rewrite header for \"%s\"", opts->unaccounted_name);
                 cleanup_state(retval, false);
                 return NULL;
@@ -356,23 +558,40 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
             hts_set_opt(retval->unaccounted_file, HTS_OPT_THREAD_POOL, &retval->p);
     }
 
-    // Open output files for RGs
-    if (!count_RG(retval->merged_input_header, &retval->output_count, &retval->rg_id)) return NULL;
-    if (opts->verbose) fprintf(samtools_stderr, "@RG's found %zu\n",retval->output_count);
+    int is_rg = !opts->tag || strcmp(opts->tag, "RG") == 0;
+    if (is_rg) {
+        if (!count_RG(retval->merged_input_header,
+                      &retval->output_count, &retval->tag_vals)) {
+            cleanup_state(retval, false);
+            return NULL;
+        }
+        if (opts->verbose)
+            fprintf(samtools_stderr, "@RG's found %zu\n",retval->output_count);
+    } else {
+        retval->output_count = 0;
+    }
+
     // Prevent calloc(0, size);
     size_t num = retval->output_count ? retval->output_count : 1;
-    retval->rg_index_file_name = (char **)calloc(num, sizeof(char *));
-    retval->rg_output_file_name = (char **)calloc(num, sizeof(char *));
-    retval->rg_output_file = (samFile**)calloc(num, sizeof(samFile*));
-    retval->rg_output_header = (sam_hdr_t**)calloc(num, sizeof(sam_hdr_t*));
-    retval->rg_hash = kh_init_c2i();
-    if (!retval->rg_output_file_name || !retval->rg_output_file || !retval->rg_output_header ||
-        !retval->rg_hash || !retval->rg_index_file_name) {
+    retval->index_file_name = (char **)calloc(num, sizeof(char *));
+    retval->output_file_name = (char **)calloc(num, sizeof(char *));
+    retval->output_file = (samFile**)calloc(num, sizeof(samFile*));
+    retval->output_header = (sam_hdr_t**)calloc(num, sizeof(sam_hdr_t*));
+    retval->tag_val_hash = kh_init_c2i();
+    if (!retval->output_file_name || !retval->output_file || !retval->output_header ||
+        !retval->tag_val_hash || !retval->index_file_name) {
         print_error_errno("split", "Could not initialise output file array");
         cleanup_state(retval, false);
         return NULL;
     }
+    if (!is_rg)
+        return retval;  // Done for this case - outputs will be opened later
 
+    // Adjust max_split if too small for the read-groups listed in the header
+    if (opts->max_split < retval->output_count)
+        opts->max_split = retval->output_count;
+
+    // Open output files for RGs
     char* dirsep = strrchr(opts->merged_input_name, '/');
     char* input_base_name = strdup(dirsep? dirsep+1 : opts->merged_input_name);
     if (!input_base_name) {
@@ -390,8 +609,8 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
 
         output_filename = expand_format_string(opts->output_format_string,
                                                input_base_name,
-                                               retval->rg_id[i], i,
-                                               &opts->ga.out);
+                                               retval->tag_vals[i], i,
+                                               opts->zero_pad, &opts->ga.out);
 
         if ( output_filename == NULL ) {
             cleanup_state(retval, false);
@@ -399,40 +618,39 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
             return NULL;
         }
 
-        retval->rg_output_file_name[i] = output_filename;
-
+        retval->output_file_name[i] = output_filename;
         sam_open_mode(outmode + 1, output_filename, NULL);
-        retval->rg_output_file[i] = sam_open_format(output_filename, outmode, &opts->ga.out);
+        retval->output_file[i] = sam_open_format(output_filename, outmode, &opts->ga.out);
 
-        if (retval->rg_output_file[i] == NULL) {
+        if (retval->output_file[i] == NULL) {
             print_error_errno("split", "Could not open \"%s\"", output_filename);
             cleanup_state(retval, false);
             free(input_base_name);
             return NULL;
         }
         if (retval->p.pool)
-            hts_set_opt(retval->rg_output_file[i], HTS_OPT_THREAD_POOL, &retval->p);
+            hts_set_opt(retval->output_file[i], HTS_OPT_THREAD_POOL, &retval->p);
 
         // Record index in hash
         int ret;
-        khiter_t iter = kh_put_c2i(retval->rg_hash, retval->rg_id[i], &ret);
+        khiter_t iter = kh_put_c2i(retval->tag_val_hash, retval->tag_vals[i], &ret);
         if (ret < 0) {
             print_error_errno("split", "Couldn't add @RG ID to look-up table");
             cleanup_state(retval, false);
             free(input_base_name);
             return NULL;
         }
-        kh_val(retval->rg_hash,iter) = i;
+        kh_val(retval->tag_val_hash,iter) = i;
 
         // Set and edit header
-        retval->rg_output_header[i] = sam_hdr_dup(retval->merged_input_header);
-        if (sam_hdr_remove_except(retval->rg_output_header[i], "RG", "ID", retval->rg_id[i]) ||
-           (!opts->no_pg &&
-            sam_hdr_add_pg(retval->rg_output_header[i], "samtools",
-                        "VN", samtools_version(),
-                        arg_list ? "CL": NULL,
-                        arg_list ? arg_list : NULL,
-                        NULL))) {
+        retval->output_header[i] = sam_hdr_dup(retval->merged_input_header);
+        if (sam_hdr_remove_except(retval->output_header[i], "RG", "ID", retval->tag_vals[i]) ||
+            (!opts->no_pg &&
+             sam_hdr_add_pg(retval->output_header[i], "samtools",
+                            "VN", samtools_version(),
+                            arg_list ? "CL": NULL,
+                            arg_list ? arg_list : NULL,
+                            NULL))) {
             print_error("split", "Could not rewrite header for \"%s\"", output_filename);
             cleanup_state(retval, false);
             free(input_base_name);
@@ -441,34 +659,30 @@ static state_t* init(parsed_opts_t* opts, const char *arg_list)
     }
 
     free(input_base_name);
-    retval->write_index = opts->ga.write_index;
 
     return retval;
 }
 
-static bool split(state_t* state)
+static bool split(state_t* state, parsed_opts_t *opts, char *arg_list)
 {
-    if (state->unaccounted_file && sam_hdr_write(state->unaccounted_file, state->unaccounted_header) != 0) {
-        print_error_errno("split", "Could not write output file header");
-        return false;
-    }
-    size_t i;
-    for (i = 0; i < state->output_count; i++) {
-        if (sam_hdr_write(state->rg_output_file[i], state->rg_output_header[i]) != 0) {
-            print_error_errno("split", "Could not write file header to \"%s\"", state->rg_output_file_name[i]);
+    int is_rg = !opts->tag || strcmp(opts->tag, "RG") == 0;
+    if (state->unaccounted_file) {
+        if (sam_hdr_write(state->unaccounted_file, state->unaccounted_header) != 0) {
+            print_error_errno("split", "Could not write output file header");
             return false;
         }
-        if (state->write_index) {
-            state->rg_index_file_name[i] = auto_index(state->rg_output_file[i],
-                                                      state->rg_output_file_name[i],
-                                                      state->rg_output_header[i]);
-            if (!state->rg_index_file_name[i]) {
-                print_error_errno("split", "Could not create index for file \"%s\"", state->rg_output_file_name[i]);
+        if (opts->ga.write_index) {
+            state->unaccounted_idx_fn = auto_index(state->unaccounted_file,
+                                                   opts->unaccounted_name,
+                                                   state->unaccounted_header);
+            if (!state->unaccounted_idx_fn) {
+                print_error_errno("split",
+                                  "Creating index file for file \"%s\" failed",
+                                  opts->unaccounted_name);
                 return false;
             }
         }
     }
-
     bam1_t* file_read = bam_init1();
     // Read the first record
     int r;
@@ -482,25 +696,79 @@ static bool split(state_t* state)
         }
     }
 
+    if (is_rg) {
+        size_t i;
+        for (i = 0; i < state->output_count; i++) {
+            if (sam_hdr_write(state->output_file[i], state->output_header[i]) != 0) {
+                print_error_errno("split", "Could not write file header to \"%s\"", state->output_file_name[i]);
+                goto error;
+            }
+            if (state->write_index) {
+                state->index_file_name[i] = auto_index(state->output_file[i],
+                        state->output_file_name[i],
+                        state->output_header[i]);
+                if (!state->index_file_name[i]) {
+                    print_error_errno("split", "Could not create index for file \"%s\"", state->output_file_name[i]);
+                    goto error;
+                }
+            }
+        }
+    }
     while (file_read != NULL) {
         // Get RG tag from read and look it up in hash to find file to output it to
-        uint8_t* tag = bam_aux_get(file_read, "RG");
+        uint8_t* tag = bam_aux_get(file_read, is_rg ? "RG" : opts->tag);
+        char *val = NULL;
+        char number[28];
         khiter_t iter;
-        if ( tag != NULL ) {
-            char* rg = bam_aux2Z(tag);
-            iter = kh_get_c2i(state->rg_hash, rg);
+        if (tag) {
+            switch (*tag) {
+            case 'Z': case 'H':
+                val = bam_aux2Z(tag);
+                break;
+            case 'c': case 'C': case 's': case 'S': case 'i': case 'I':
+                if (opts->zero_pad == 0) {
+                    snprintf(number, sizeof(number), "%"PRId64, bam_aux2i(tag));
+                } else {
+                    int64_t v = bam_aux2i(tag);
+                    snprintf(number, sizeof(number), "%0*"PRId64,
+                             v < 0 ? opts->zero_pad + 1 : opts->zero_pad, v);
+                }
+                val = number;
+                break;
+            default:
+                break;
+            }
+        }
+        if ( val != NULL ) {
+            iter = kh_get_c2i(state->tag_val_hash, val);
         } else {
-            iter = kh_end(state->rg_hash);
+            iter = kh_end(state->tag_val_hash);
+        }
+
+        // Check for opts->tag here instead of !is_rg so we open new
+        // files if the user specified '-d RG' and we find a RG:Z: value
+        // that wasn't listed in the header.  If the '-d' option is
+        // not used, we don't open a file to preserve existing behaviour.
+        if (opts->tag && val && iter == kh_end(state->tag_val_hash)
+            && state->output_count < opts->max_split) {
+            // Need to open a new output file
+            iter = prep_sam_file(opts, state, val, arg_list, is_rg);
+            if (iter == kh_end(state->tag_val_hash)) { // Open failed
+                print_error("split",
+                            "Could not create output file for tag \"%s:%s\"",
+                            opts->tag, bam_aux2Z(tag));
+                goto error;
+
+            }
         }
 
         // Write the read out to correct file
-        if (iter != kh_end(state->rg_hash)) {
+        if (iter != kh_end(state->tag_val_hash)) {
             // if found write to the appropriate untangled bam
-            int i = kh_val(state->rg_hash,iter);
-            if (sam_write1(state->rg_output_file[i], state->rg_output_header[i], file_read) < 0) {
-                print_error_errno("split", "Could not write to \"%s\"", state->rg_output_file_name[i]);
-                bam_destroy1(file_read);
-                return false;
+            int i = kh_val(state->tag_val_hash,iter);
+            if (sam_write1(state->output_file[i], state->output_header[i], file_read) < 0) {
+                print_error_errno("split", "Could not write to \"%s\"", state->output_file_name[i]);
+                goto error;
             }
         } else {
             // otherwise write to the unaccounted bam if there is one or fail
@@ -508,15 +776,14 @@ static bool split(state_t* state)
                 if (tag) {
                     fprintf(samtools_stderr, "Read \"%s\" with unaccounted for tag \"%s\".\n", bam_get_qname(file_read), bam_aux2Z(tag));
                 } else {
-                    fprintf(samtools_stderr, "Read \"%s\" has no RG tag.\n", bam_get_qname(file_read));
+                    fprintf(samtools_stderr, "Read \"%s\" has no %s tag.\n",
+                            bam_get_qname(file_read), is_rg ? "RG" : opts->tag);
                 }
-                bam_destroy1(file_read);
-                return false;
+                goto error;
             } else {
                 if (sam_write1(state->unaccounted_file, state->unaccounted_header, file_read) < 0) {
                     print_error_errno("split", "Could not write to unaccounted output file");
-                    bam_destroy1(file_read);
-                    return false;
+                    goto error;
                 }
             }
         }
@@ -534,16 +801,27 @@ static bool split(state_t* state)
     }
 
     if (state->write_index) {
+        size_t i;
         for (i = 0; i < state->output_count; i++) {
-            if (sam_idx_save(state->rg_output_file[i]) < 0) {
-                print_error_errno("split", "writing index failed");
+            if (sam_idx_save(state->output_file[i]) < 0) {
+                print_error_errno("split", "writing index \"%s\" failed",
+                                  state->index_file_name[i]);
+                return false;
+            }
+        }
+        if (state->unaccounted_file) {
+            if (sam_idx_save(state->unaccounted_file) < 0) {
+                print_error_errno("split", "writing index \"%s\" failed",
+                                  state->unaccounted_idx_fn);
                 return false;
             }
-            free(state->rg_index_file_name[i]);
         }
     }
 
     return true;
+error:
+    bam_destroy1(file_read);
+    return false;
 }
 
 static int cleanup_state(state_t* status, bool check_close)
@@ -561,25 +839,28 @@ static int cleanup_state(state_t* status, bool check_close)
     sam_close(status->merged_input_file);
     size_t i;
     for (i = 0; i < status->output_count; i++) {
-        if (status->rg_output_header && status->rg_output_header[i])
-            sam_hdr_destroy(status->rg_output_header[i]);
-        if (status->rg_output_file && status->rg_output_file[i]) {
-            if (sam_close(status->rg_output_file[i]) < 0 && check_close) {
-                print_error("split", "Error on closing output file \"%s\"", status->rg_output_file_name[i]);
+        if (status->output_header && status->output_header[i])
+            sam_hdr_destroy(status->output_header[i]);
+        if (status->output_file && status->output_file[i]) {
+            if (sam_close(status->output_file[i]) < 0 && check_close) {
+                print_error("split", "Error on closing output file \"%s\"", status->output_file_name[i]);
                 ret = -1;
             }
         }
-        if (status->rg_id) free(status->rg_id[i]);
-        if (status->rg_output_file_name) free(status->rg_output_file_name[i]);
+        if (status->tag_vals) free(status->tag_vals[i]);
+        if (status->output_file_name) free(status->output_file_name[i]);
+        if (status->index_file_name[i]) free(status->index_file_name[i]);
     }
     if (status->merged_input_header)
         sam_hdr_destroy(status->merged_input_header);
-    free(status->rg_output_header);
-    free(status->rg_output_file);
-    free(status->rg_output_file_name);
-    free(status->rg_index_file_name);
-    kh_destroy_c2i(status->rg_hash);
-    free(status->rg_id);
+    free(status->output_header);
+    free(status->output_file);
+    free(status->output_file_name);
+    free(status->index_file_name);
+    free(status->unaccounted_idx_fn);
+    kh_destroy_c2i(status->tag_val_hash);
+
+    free(status->tag_vals);
     if (status->p.pool)
         hts_tpool_destroy(status->p.pool);
     free(status);
@@ -605,7 +886,7 @@ int main_split(int argc, char** argv)
     state_t* status = init(opts, arg_list);
     if (!status) goto cleanup_opts;
 
-    if (!split(status)) {
+    if (!split(status, opts, arg_list)) {
         cleanup_state(status, false);
         goto cleanup_opts;
     }
index e05ea18162885b249c7f4b4c544e2e1b99162d68..8c330bc2b869aaabc4a0ce7423251a27eccd8cc9 100644 (file)
@@ -75,7 +75,7 @@ int main_reference(int argc, char *argv[]);
 int main_reset(int argc, char *argv[]);
 int main_cram_size(int argc, char *argv[]);
 
-const char *samtools_version()
+const char *samtools_version(void)
 {
     return SAMTOOLS_VERSION;
 }
@@ -103,7 +103,7 @@ const char *samtools_feature_string(void) {
 static void long_version(void) {
     printf("samtools %s\n"
            "Using htslib %s\n"
-           "Copyright (C) 2023 Genome Research Ltd.\n",
+           "Copyright (C) 2024 Genome Research Ltd.\n",
            samtools_version(), hts_version());
 
     printf("\nSamtools compilation details:\n");
@@ -303,5 +303,14 @@ int main(int argc, char *argv[])
         fprintf(stderr, "[main] unrecognized command '%s'\n", argv[1]);
         return 1;
     }
+
+    // For subcommands that may have produced substantial output on stdout,
+    // make a final check for delayed I/O errors. Ignore EBADF as other code
+    // may have already closed stdout.
+    if (fclose(stdout) != 0 && errno != EBADF) {
+        print_error_errno(argv[1], "closing standard output failed");
+        return 1;
+    }
+
     return ret;
 }
index d95ec05bb569da245ec3f3462b61c747efee0800..a76729ce6924138aad834dad8d7af23f14b8db07 100644 (file)
@@ -78,7 +78,7 @@ int main_reference(int argc, char *argv[]);
 int main_reset(int argc, char *argv[]);
 int main_cram_size(int argc, char *argv[]);
 
-const char *samtools_version()
+const char *samtools_version(void)
 {
     return SAMTOOLS_VERSION;
 }
@@ -106,7 +106,7 @@ const char *samtools_feature_string(void) {
 static void long_version(void) {
     fprintf(samtools_stdout, "samtools %s\n"
            "Using htslib %s\n"
-           "Copyright (C) 2023 Genome Research Ltd.\n",
+           "Copyright (C) 2024 Genome Research Ltd.\n",
            samtools_version(), hts_version());
 
     fprintf(samtools_stdout, "\nSamtools compilation details:\n");
@@ -306,5 +306,14 @@ int samtools_main(int argc, char *argv[])
         fprintf(samtools_stderr, "[main] unrecognized command '%s'\n", argv[1]);
         return 1;
     }
+
+    // For subcommands that may have produced substantial output on samtools_stdout,
+    // make a final check for delayed I/O errors. Ignore EBADF as other code
+    // may have already closed samtools_stdout.
+    if (0) { //if (fclose(samtools_stdout) != 0 && errno != EBADF) {
+        print_error_errno(argv[1], "closing standard output failed");
+        return 1;
+    }
+
     return ret;
 }
index 1bd46a0eb110741ae666a088532a991e668bf7aa..10eeface69aba43bfbbca827a1debecd85e95126 100644 (file)
@@ -1,7 +1,7 @@
 /*  bedcov.c -- bedcov subcommand.
 
     Copyright (C) 2012 Broad Institute.
-    Copyright (C) 2013-2014, 2018-2022 Genome Research Ltd.
+    Copyright (C) 2013-2014, 2018-2022, 2024 Genome Research Ltd.
 
     Author: Heng Li <lh3@sanger.ac.uk>
 
@@ -40,7 +40,7 @@ DEALINGS IN THE SOFTWARE.  */
 #include "htslib/kseq.h"
 KSTREAM_INIT(gzFile, gzread, 16384)
 
-#define DEFAULT_DEPTH 64000
+#define DEFAULT_DEPTH INT_MAX
 
 typedef struct {
     htsFile *fp;
@@ -85,21 +85,23 @@ int main_bedcov(int argc, char *argv[])
     const bam_pileup1_t **plp;
     int usage = 0, has_index_file = 0;
     uint32_t flags = (BAM_FUNMAP | BAM_FSECONDARY | BAM_FQCFAIL | BAM_FDUP);
-    int tflags = 0, min_depth = -1;
+    int tflags = 0, min_depth = -1, max_depth = DEFAULT_DEPTH, print_header=0;
 
     sam_global_args ga = SAM_GLOBAL_ARGS_INIT;
     static const struct option lopts[] = {
         {"min-MQ", required_argument, NULL, 'Q'},
         {"min-mq", required_argument, NULL, 'Q'},
+        {"max-depth", required_argument, NULL, 'd'+1000},
         SAM_OPT_GLOBAL_OPTIONS('-', 0, '-', '-', 0, '-'),
         { NULL, 0, NULL, 0 }
     };
 
-    while ((c = getopt_long(argc, argv, "Q:Xg:G:jd:c", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "Q:Xg:G:jd:Hc", lopts, NULL)) >= 0) {
         switch (c) {
         case 'Q': min_mapQ = atoi(optarg); break;
         case 'X': has_index_file = 1; break;
         case 'c': do_rcount = 1; break;
+        case 'H': print_header = 1; break;
         case 'g':
             tflags = bam_str2flag(optarg);
             if (tflags < 0 || tflags > ((BAM_FSUPPLEMENTARY << 1) - 1)) {
@@ -118,6 +120,7 @@ int main_bedcov(int argc, char *argv[])
             break;
         case 'j': skip_DN = 1; break;
         case 'd': min_depth = atoi(optarg); break;
+        case 'd'+1000: max_depth = atoi(optarg); break;
         default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
                   /* else fall-through */
         case '?': usage = 1; break;
@@ -133,9 +136,11 @@ int main_bedcov(int argc, char *argv[])
         fprintf(stderr, "      -G <flags>          add the specified flags to the set used to filter out reads\n"
                         "                          The default set is UNMAP,SECONDARY,QCFAIL,DUP or 0x704\n");
         fprintf(stderr, "      -j                  do not include deletions (D) and ref skips (N) in bedcov computation\n");
-        fprintf(stderr, "      -d <int>            depth threshold. Number of reference bases with coverage above and"
+        fprintf(stderr, "      --max-depth <int>   sets the maximum depth used in the mpileup algorithm\n");
+        fprintf(stderr, "      -d <int>            depth threshold. Number of reference bases with coverage above and\n"
                         "                          including this value will be displayed in a separate column\n");
         fprintf(stderr, "      -c                  add an additional column showing read count\n");
+        fprintf(stderr, "      -H                  print a comment/header line with column information.\n");
         sam_global_opt_help(stderr, "-.--.--.");
         return 1;
     }
@@ -190,6 +195,23 @@ int main_bedcov(int argc, char *argv[])
         print_error_errno("bedcov", "can't open BED file '%s'", argv[optind]);
         return 2;
     }
+
+    if (print_header) {
+        printf("#chrom\tstart\tend");
+        for (i = 0; i < n; ++i) {
+            printf("\t%s_cov", argv[i+optind+1]);
+        }
+        if (min_depth >= 0) {
+            for (i = 0; i < n; ++i)
+                printf("\t%s_depth", argv[i+optind+1]);
+        }
+        if (do_rcount) {
+            for (i = 0; i < n; ++i)
+                printf("\t%s_count", argv[i+optind+1]);
+        }
+        putchar('\n');
+    }
+
     ks = ks_init(fp);
     n_plp = calloc(n, sizeof(int));
     plp = calloc(n, sizeof(bam_pileup1_t*));
@@ -220,10 +242,11 @@ int main_bedcov(int argc, char *argv[])
         }
 
         mplp = bam_mplp_init(n, read_bam, (void**)aux);
-        if (min_depth > DEFAULT_DEPTH)
+        if (min_depth > max_depth)
+            // NB: never happens given current DEFAULT_DEPTH of INT_MAX
             bam_mplp_set_maxcnt(mplp, min_depth);
         else
-            bam_mplp_set_maxcnt(mplp, DEFAULT_DEPTH);
+            bam_mplp_set_maxcnt(mplp, max_depth);
 
         memset(cnt, 0, sizeof(*cnt) * n);
         if (min_depth >= 0) memset(pcov, 0, sizeof(*pcov) * n);
index f259cb185cfa8ee034b39f533dc1986795a69cb8..c6f0cccd60400af4047e826498ab430c821daa13 100644 (file)
@@ -3,7 +3,7 @@
 /*  bedcov.c -- bedcov subcommand.
 
     Copyright (C) 2012 Broad Institute.
-    Copyright (C) 2013-2014, 2018-2022 Genome Research Ltd.
+    Copyright (C) 2013-2014, 2018-2022, 2024 Genome Research Ltd.
 
     Author: Heng Li <lh3@sanger.ac.uk>
 
@@ -42,7 +42,7 @@ DEALINGS IN THE SOFTWARE.  */
 #include "htslib/kseq.h"
 KSTREAM_INIT(gzFile, gzread, 16384)
 
-#define DEFAULT_DEPTH 64000
+#define DEFAULT_DEPTH INT_MAX
 
 typedef struct {
     htsFile *fp;
@@ -87,21 +87,23 @@ int main_bedcov(int argc, char *argv[])
     const bam_pileup1_t **plp;
     int usage = 0, has_index_file = 0;
     uint32_t flags = (BAM_FUNMAP | BAM_FSECONDARY | BAM_FQCFAIL | BAM_FDUP);
-    int tflags = 0, min_depth = -1;
+    int tflags = 0, min_depth = -1, max_depth = DEFAULT_DEPTH, print_header=0;
 
     sam_global_args ga = SAM_GLOBAL_ARGS_INIT;
     static const struct option lopts[] = {
         {"min-MQ", required_argument, NULL, 'Q'},
         {"min-mq", required_argument, NULL, 'Q'},
+        {"max-depth", required_argument, NULL, 'd'+1000},
         SAM_OPT_GLOBAL_OPTIONS('-', 0, '-', '-', 0, '-'),
         { NULL, 0, NULL, 0 }
     };
 
-    while ((c = getopt_long(argc, argv, "Q:Xg:G:jd:c", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "Q:Xg:G:jd:Hc", lopts, NULL)) >= 0) {
         switch (c) {
         case 'Q': min_mapQ = atoi(optarg); break;
         case 'X': has_index_file = 1; break;
         case 'c': do_rcount = 1; break;
+        case 'H': print_header = 1; break;
         case 'g':
             tflags = bam_str2flag(optarg);
             if (tflags < 0 || tflags > ((BAM_FSUPPLEMENTARY << 1) - 1)) {
@@ -120,6 +122,7 @@ int main_bedcov(int argc, char *argv[])
             break;
         case 'j': skip_DN = 1; break;
         case 'd': min_depth = atoi(optarg); break;
+        case 'd'+1000: max_depth = atoi(optarg); break;
         default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
                   /* else fall-through */
         case '?': usage = 1; break;
@@ -135,9 +138,11 @@ int main_bedcov(int argc, char *argv[])
         fprintf(samtools_stderr, "      -G <flags>          add the specified flags to the set used to filter out reads\n"
                         "                          The default set is UNMAP,SECONDARY,QCFAIL,DUP or 0x704\n");
         fprintf(samtools_stderr, "      -j                  do not include deletions (D) and ref skips (N) in bedcov computation\n");
-        fprintf(samtools_stderr, "      -d <int>            depth threshold. Number of reference bases with coverage above and"
+        fprintf(samtools_stderr, "      --max-depth <int>   sets the maximum depth used in the mpileup algorithm\n");
+        fprintf(samtools_stderr, "      -d <int>            depth threshold. Number of reference bases with coverage above and\n"
                         "                          including this value will be displayed in a separate column\n");
         fprintf(samtools_stderr, "      -c                  add an additional column showing read count\n");
+        fprintf(samtools_stderr, "      -H                  print a comment/header line with column information.\n");
         sam_global_opt_help(samtools_stderr, "-.--.--.");
         return 1;
     }
@@ -192,6 +197,23 @@ int main_bedcov(int argc, char *argv[])
         print_error_errno("bedcov", "can't open BED file '%s'", argv[optind]);
         return 2;
     }
+
+    if (print_header) {
+        fprintf(samtools_stdout, "#chrom\tstart\tend");
+        for (i = 0; i < n; ++i) {
+            fprintf(samtools_stdout, "\t%s_cov", argv[i+optind+1]);
+        }
+        if (min_depth >= 0) {
+            for (i = 0; i < n; ++i)
+                fprintf(samtools_stdout, "\t%s_depth", argv[i+optind+1]);
+        }
+        if (do_rcount) {
+            for (i = 0; i < n; ++i)
+                fprintf(samtools_stdout, "\t%s_count", argv[i+optind+1]);
+        }
+        fputc('\n', samtools_stdout);
+    }
+
     ks = ks_init(fp);
     n_plp = calloc(n, sizeof(int));
     plp = calloc(n, sizeof(bam_pileup1_t*));
@@ -222,10 +244,11 @@ int main_bedcov(int argc, char *argv[])
         }
 
         mplp = bam_mplp_init(n, read_bam, (void**)aux);
-        if (min_depth > DEFAULT_DEPTH)
+        if (min_depth > max_depth)
+            // NB: never happens given current DEFAULT_DEPTH of INT_MAX
             bam_mplp_set_maxcnt(mplp, min_depth);
         else
-            bam_mplp_set_maxcnt(mplp, DEFAULT_DEPTH);
+            bam_mplp_set_maxcnt(mplp, max_depth);
 
         memset(cnt, 0, sizeof(*cnt) * n);
         if (min_depth >= 0) memset(pcov, 0, sizeof(*pcov) * n);
index 6b22d4efce1f9a8aa5c499a8fc94a136aff1a4f6..0fe5e726273628e3e74b64ffe2d926c0333623f1 100644 (file)
@@ -1,7 +1,7 @@
 /*  bedidx.c -- BED file indexing.
 
     Copyright (C) 2011 Broad Institute.
-    Copyright (C) 2014, 2017-2019 Genome Research Ltd.
+    Copyright (C) 2014, 2017-2019, 2024 Genome Research Ltd.
 
     Author: Heng Li <lh3@sanger.ac.uk>
 
@@ -58,6 +58,7 @@ typedef struct {
     hts_pair_pos_t *a;
     int *idx;
     int filter;
+    hts_pos_t max_idx;
 } bed_reglist_t;
 
 #include "htslib/khash.h"
@@ -96,63 +97,68 @@ static void bed_print(void *reg_hash) {
 }
 #endif
 
-static int *bed_index_core(int n, hts_pair_pos_t *a)
+static int bed_index_core(bed_reglist_t *regions)
 {
-    int i, j, l, *idx, *new_idx;
-    l = 0; idx = 0;
-    for (i = 0; i < n; ++i) {
-        hts_pos_t beg, end;
-        beg = a[i].beg >> LIDX_SHIFT; end = a[i].end >> LIDX_SHIFT;
-        if (l < end + 1) {
-            int old_l = l;
-            l = end + 1;
-            kroundup32(l);
-            new_idx = realloc(idx, l * sizeof(*idx));
-            if (!new_idx) {
-                free(idx);
-                return NULL;
-            }
-            idx = new_idx;
-
-            for (j = old_l; j < l; ++j)
-                idx[j] = -1;
+    int i, *idx = NULL;
+    size_t idx_size = 0;
+    hts_pos_t last_end = 0;
+    hts_pair_pos_t *a = regions->a;
+
+    // Construct a linear index on regions, to allow rapid lookup of
+    // where to start searching for matches
+    for (i = 0; i < regions->n; ++i) {
+        hts_pos_t beg = a[i].beg >= 0 ? a[i].beg >> LIDX_SHIFT : 0;
+        hts_pos_t end = a[i].end >= 0 ? a[i].end >> LIDX_SHIFT : 0;
+        hts_pos_t j;
+        if (end < last_end)
+            continue;  // Can happen for a containment
+        if (end + 1 >= SIZE_MAX / sizeof(*idx)) { // Ensure no overflow
+            errno = ENOMEM;
+            free(idx);
+            return -1;
         }
-
-        for (j = beg; j < end+1; ++j)
-            if (idx[j] < 0)
-                idx[j] = i;
+        if (hts_resize(int, (size_t) end + 1, &idx_size, &idx, 0) < 0) {
+            free(idx);
+            return -1;
+        }
+        // Fill any gap prior to this region by pointing to the previous one
+        for (j = last_end; j < beg; j++)
+            idx[j] = i > 0 ? i - 1 : 0;
+        // Fill from max(last_end, beg) to `end` (inclusive) with current region
+        for (; j <= end; j++)
+            idx[j] = i;
+        // Remember where finished for the next gap
+        last_end = end + 1;
     }
-    return idx;
+    regions->idx = idx;
+    regions->max_idx = last_end;
+    return 0;
 }
 
-static void bed_index(void *_h)
+static int bed_index(reghash_t *h)
 {
-    reghash_t *h = (reghash_t*)_h;
     khint_t k;
     for (k = 0; k < kh_end(h); ++k) {
         if (kh_exist(h, k)) {
             bed_reglist_t *p = &kh_val(h, k);
-            if (p->idx) free(p->idx);
+            if (p->idx) {
+                free(p->idx);
+                p->idx = NULL;
+            }
             ks_introsort(hts_pair_pos_t, p->n, p->a);
-            p->idx = bed_index_core(p->n, p->a);
+            if (bed_index_core(p) != 0) {
+                return -1;
+            }
         }
     }
+    return 0;
 }
 
-static int bed_minoff(const bed_reglist_t *p, hts_pos_t beg, hts_pos_t end) {
-    int i, min_off=0;
-
-    if (p && p->idx) {
-        min_off = (beg>>LIDX_SHIFT >= p->n)? p->idx[p->n-1] : p->idx[beg>>LIDX_SHIFT];
-        if (min_off < 0) { // TODO: this block can be improved, but speed should not matter too much here
-            hts_pos_t n = beg>>LIDX_SHIFT;
-            if (n > p->n)
-                n = p->n;
-            for (i = n - 1; i >= 0; --i)
-                if (p->idx[i] >= 0)
-                    break;
-            min_off = i >= 0? p->idx[i] : 0;
-        }
+static int bed_minoff(const bed_reglist_t *p, hts_pos_t beg) {
+    int min_off=0;
+
+    if (p && p->idx && p->max_idx > 0 && beg >= 0) {
+        min_off = (beg>>LIDX_SHIFT >= p->max_idx)? p->idx[p->max_idx-1] : p->idx[beg>>LIDX_SHIFT];
     }
 
     return min_off;
@@ -162,7 +168,7 @@ static int bed_overlap_core(const bed_reglist_t *p, hts_pos_t beg, hts_pos_t end
 {
     int i, min_off;
     if (p->n == 0) return 0;
-    min_off = bed_minoff(p, beg, end);
+    min_off = bed_minoff(p, beg);
 
     for (i = min_off; i < p->n; ++i) {
         if (p->a[i].beg >= end) break; // out of range; no need to proceed
@@ -259,7 +265,7 @@ void *bed_read(const char *fn)
     if (NULL == h) return NULL;
     // read the list
     fp = strcmp(fn, "-")? gzopen(fn, "r") : gzdopen(fileno(stdin), "r");
-    if (fp == 0) return 0;
+    if (fp == 0) goto fail;
     ks = ks_init(fp);
     if (NULL == ks) goto fail;  // In case ks_init ever gets error checking...
     int ks_len;
@@ -339,9 +345,10 @@ void *bed_read(const char *fn)
         fp = NULL;
         goto fail;
     }
+    if (bed_index(h) != 0)
+        goto fail;
     ks_destroy(ks);
     free(str.s);
-    bed_index(h);
     //bed_unify(h);
     return h;
  fail:
@@ -457,7 +464,7 @@ static void *bed_filter(void *reg_hash, void *tmp_hash) {
             beg = q->a[i].beg;
             end = q->a[i].end;
 
-            min_off = bed_minoff(p, beg, end);
+            min_off = bed_minoff(p, beg);
             for (j = min_off; j < p->n; ++j) {
                 if (p->a[j].beg >= end) break; // out of range; no need to proceed
                 if (p->a[j].end > beg && p->a[j].beg < end) {
@@ -534,28 +541,43 @@ void *bed_hash_regions(void *reg_hash, char **regs, int first, int last, int *op
 
         //if op==1 insert reg to the bed hash table
         if (*op && !(bed_insert(h, reg, beg, end))) {
-            fprintf(stderr, "Error when inserting region='%s' in the bed hash table at address=%p!\n", regs[i], h);
+            fprintf(stderr, "Error when inserting region='%s' in the bed hash table at address=%p!\n", regs[i], (void *)h);
         }
         //if op==0, first insert the regions in the temporary hash table,
         //then filter the bed hash table using it
         if (!(*op) && !(bed_insert(t, reg, beg, end))) {
-            fprintf(stderr, "Error when inserting region='%s' in the temporary hash table at address=%p!\n", regs[i], t);
+            fprintf(stderr, "Error when inserting region='%s' in the temporary hash table at address=%p!\n", regs[i], (void *)t);
         }
     }
 
     if (!(*op)) {
-        bed_index(t);
+        if (bed_index(t) != 0)
+            goto fail;
         bed_unify(t);
-        h = bed_filter(h, t);
+        if (bed_filter(h, t) == NULL)
+            goto fail;
         bed_destroy(t);
+        t = NULL;
     }
 
     if (h) {
-        bed_index(h);
+        if (bed_index(h) != 0)
+            goto fail;
         bed_unify(h);
     }
 
     return h;
+
+ fail:
+    // Clean up whichever hash we made
+    if (reg_hash) {
+        if (t)
+            bed_destroy(t);
+    } else {
+        if (h)
+            bed_destroy(h);
+    }
+    return NULL;
 }
 
 const char* bed_get(void *reg_hash, int i, int filter) {
index 533b42a92c202b0d367a169cbeff9c8f6734fcf3..e62ab51439a10d909d234f8e31898adf373d5b2c 100644 (file)
@@ -3,7 +3,7 @@
 /*  bedidx.c -- BED file indexing.
 
     Copyright (C) 2011 Broad Institute.
-    Copyright (C) 2014, 2017-2019 Genome Research Ltd.
+    Copyright (C) 2014, 2017-2019, 2024 Genome Research Ltd.
 
     Author: Heng Li <lh3@sanger.ac.uk>
 
@@ -60,6 +60,7 @@ typedef struct {
     hts_pair_pos_t *a;
     int *idx;
     int filter;
+    hts_pos_t max_idx;
 } bed_reglist_t;
 
 #include "htslib/khash.h"
@@ -98,63 +99,68 @@ static void bed_print(void *reg_hash) {
 }
 #endif
 
-static int *bed_index_core(int n, hts_pair_pos_t *a)
+static int bed_index_core(bed_reglist_t *regions)
 {
-    int i, j, l, *idx, *new_idx;
-    l = 0; idx = 0;
-    for (i = 0; i < n; ++i) {
-        hts_pos_t beg, end;
-        beg = a[i].beg >> LIDX_SHIFT; end = a[i].end >> LIDX_SHIFT;
-        if (l < end + 1) {
-            int old_l = l;
-            l = end + 1;
-            kroundup32(l);
-            new_idx = realloc(idx, l * sizeof(*idx));
-            if (!new_idx) {
-                free(idx);
-                return NULL;
-            }
-            idx = new_idx;
-
-            for (j = old_l; j < l; ++j)
-                idx[j] = -1;
+    int i, *idx = NULL;
+    size_t idx_size = 0;
+    hts_pos_t last_end = 0;
+    hts_pair_pos_t *a = regions->a;
+
+    // Construct a linear index on regions, to allow rapid lookup of
+    // where to start searching for matches
+    for (i = 0; i < regions->n; ++i) {
+        hts_pos_t beg = a[i].beg >= 0 ? a[i].beg >> LIDX_SHIFT : 0;
+        hts_pos_t end = a[i].end >= 0 ? a[i].end >> LIDX_SHIFT : 0;
+        hts_pos_t j;
+        if (end < last_end)
+            continue;  // Can happen for a containment
+        if (end + 1 >= SIZE_MAX / sizeof(*idx)) { // Ensure no overflow
+            errno = ENOMEM;
+            free(idx);
+            return -1;
         }
-
-        for (j = beg; j < end+1; ++j)
-            if (idx[j] < 0)
-                idx[j] = i;
+        if (hts_resize(int, (size_t) end + 1, &idx_size, &idx, 0) < 0) {
+            free(idx);
+            return -1;
+        }
+        // Fill any gap prior to this region by pointing to the previous one
+        for (j = last_end; j < beg; j++)
+            idx[j] = i > 0 ? i - 1 : 0;
+        // Fill from max(last_end, beg) to `end` (inclusive) with current region
+        for (; j <= end; j++)
+            idx[j] = i;
+        // Remember where finished for the next gap
+        last_end = end + 1;
     }
-    return idx;
+    regions->idx = idx;
+    regions->max_idx = last_end;
+    return 0;
 }
 
-static void bed_index(void *_h)
+static int bed_index(reghash_t *h)
 {
-    reghash_t *h = (reghash_t*)_h;
     khint_t k;
     for (k = 0; k < kh_end(h); ++k) {
         if (kh_exist(h, k)) {
             bed_reglist_t *p = &kh_val(h, k);
-            if (p->idx) free(p->idx);
+            if (p->idx) {
+                free(p->idx);
+                p->idx = NULL;
+            }
             ks_introsort(hts_pair_pos_t, p->n, p->a);
-            p->idx = bed_index_core(p->n, p->a);
+            if (bed_index_core(p) != 0) {
+                return -1;
+            }
         }
     }
+    return 0;
 }
 
-static int bed_minoff(const bed_reglist_t *p, hts_pos_t beg, hts_pos_t end) {
-    int i, min_off=0;
-
-    if (p && p->idx) {
-        min_off = (beg>>LIDX_SHIFT >= p->n)? p->idx[p->n-1] : p->idx[beg>>LIDX_SHIFT];
-        if (min_off < 0) { // TODO: this block can be improved, but speed should not matter too much here
-            hts_pos_t n = beg>>LIDX_SHIFT;
-            if (n > p->n)
-                n = p->n;
-            for (i = n - 1; i >= 0; --i)
-                if (p->idx[i] >= 0)
-                    break;
-            min_off = i >= 0? p->idx[i] : 0;
-        }
+static int bed_minoff(const bed_reglist_t *p, hts_pos_t beg) {
+    int min_off=0;
+
+    if (p && p->idx && p->max_idx > 0 && beg >= 0) {
+        min_off = (beg>>LIDX_SHIFT >= p->max_idx)? p->idx[p->max_idx-1] : p->idx[beg>>LIDX_SHIFT];
     }
 
     return min_off;
@@ -164,7 +170,7 @@ static int bed_overlap_core(const bed_reglist_t *p, hts_pos_t beg, hts_pos_t end
 {
     int i, min_off;
     if (p->n == 0) return 0;
-    min_off = bed_minoff(p, beg, end);
+    min_off = bed_minoff(p, beg);
 
     for (i = min_off; i < p->n; ++i) {
         if (p->a[i].beg >= end) break; // out of range; no need to proceed
@@ -261,7 +267,7 @@ void *bed_read(const char *fn)
     if (NULL == h) return NULL;
     // read the list
     fp = strcmp(fn, "-")? gzopen(fn, "r") : gzdopen(fileno(stdin), "r");
-    if (fp == 0) return 0;
+    if (fp == 0) goto fail;
     ks = ks_init(fp);
     if (NULL == ks) goto fail;  // In case ks_init ever gets error checking...
     int ks_len;
@@ -341,9 +347,10 @@ void *bed_read(const char *fn)
         fp = NULL;
         goto fail;
     }
+    if (bed_index(h) != 0)
+        goto fail;
     ks_destroy(ks);
     free(str.s);
-    bed_index(h);
     //bed_unify(h);
     return h;
  fail:
@@ -459,7 +466,7 @@ static void *bed_filter(void *reg_hash, void *tmp_hash) {
             beg = q->a[i].beg;
             end = q->a[i].end;
 
-            min_off = bed_minoff(p, beg, end);
+            min_off = bed_minoff(p, beg);
             for (j = min_off; j < p->n; ++j) {
                 if (p->a[j].beg >= end) break; // out of range; no need to proceed
                 if (p->a[j].end > beg && p->a[j].beg < end) {
@@ -536,28 +543,43 @@ void *bed_hash_regions(void *reg_hash, char **regs, int first, int last, int *op
 
         //if op==1 insert reg to the bed hash table
         if (*op && !(bed_insert(h, reg, beg, end))) {
-            fprintf(samtools_stderr, "Error when inserting region='%s' in the bed hash table at address=%p!\n", regs[i], h);
+            fprintf(samtools_stderr, "Error when inserting region='%s' in the bed hash table at address=%p!\n", regs[i], (void *)h);
         }
         //if op==0, first insert the regions in the temporary hash table,
         //then filter the bed hash table using it
         if (!(*op) && !(bed_insert(t, reg, beg, end))) {
-            fprintf(samtools_stderr, "Error when inserting region='%s' in the temporary hash table at address=%p!\n", regs[i], t);
+            fprintf(samtools_stderr, "Error when inserting region='%s' in the temporary hash table at address=%p!\n", regs[i], (void *)t);
         }
     }
 
     if (!(*op)) {
-        bed_index(t);
+        if (bed_index(t) != 0)
+            goto fail;
         bed_unify(t);
-        h = bed_filter(h, t);
+        if (bed_filter(h, t) == NULL)
+            goto fail;
         bed_destroy(t);
+        t = NULL;
     }
 
     if (h) {
-        bed_index(h);
+        if (bed_index(h) != 0)
+            goto fail;
         bed_unify(h);
     }
 
     return h;
+
+ fail:
+    // Clean up whichever hash we made
+    if (reg_hash) {
+        if (t)
+            bed_destroy(t);
+    } else {
+        if (h)
+            bed_destroy(h);
+    }
+    return NULL;
 }
 
 const char* bed_get(void *reg_hash, int i, int filter) {
index dedaa8e991304391412ad61c34a9195b5b1a38c0..92f497dc885a2438a68720ce088bf0e7119712dc 100644 (file)
@@ -1,7 +1,7 @@
 /* coverage.c -- samtools coverage subcommand
 
     Copyright (C) 2018,2019 Florian Breitwieser
-    Portions copyright (C) 2019-2021 Genome Research Ltd.
+    Portions copyright (C) 2019-2021, 2023-2024 Genome Research Ltd.
 
     Author: Florian P Breitwieser <florian.bw@gmail.com>
 
@@ -60,6 +60,7 @@ typedef struct {  // auxiliary data structure to hold stats on coverage
     unsigned long long summed_coverage;
     unsigned long long summed_baseQ;
     unsigned long long summed_mapQ;
+    unsigned long long quality_bases;
     unsigned int n_reads;
     unsigned int n_selected_reads;
     bool covered;
@@ -105,7 +106,7 @@ static const char *const BLOCK_CHARS2[2] = {".", ":"};
 // in bam_plcmd.c
 int read_file_list(const char *file_list, int *n, char **argv[]);
 
-static int usage() {
+static int usage(void) {
     fprintf(stdout, "Usage: samtools coverage [options] in1.bam [in2.bam [...]]\n\n"
             "Input options:\n"
             "  -b, --bam-list FILE     list of input BAM filenames, one per line\n"
@@ -120,6 +121,7 @@ static int usage() {
             "                          effectively removing any depth limit.\n"
             "Output options:\n"
             "  -m, --histogram         show histogram instead of tabular output\n"
+            "  -D, --plot-depth        plot depth instead of tabular output\n"
             "  -A, --ascii             show only ASCII characters in histogram\n"
             "  -o, --output FILE       write output to FILE [stdout]\n"
             "  -H, --no-header         don't print a header in tabular mode\n"
@@ -203,13 +205,13 @@ void print_tabular_line(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *s
             stats[tid].n_covered_bases,
             100.0 * stats[tid].n_covered_bases / region_len,
             stats[tid].summed_coverage / region_len,
-            stats[tid].summed_coverage > 0? stats[tid].summed_baseQ/(double) stats[tid].summed_coverage : 0,
+            stats[tid].quality_bases > 0? stats[tid].summed_baseQ/(double) stats[tid].quality_bases : 0,
             stats[tid].n_selected_reads > 0? stats[tid].summed_mapQ/(double) stats[tid].n_selected_reads : 0
            );
 }
 
 void print_hist(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *stats, int tid, const uint32_t *hist,
-        const int hist_size, const bool full_utf) {
+        const int hist_size, const bool full_utf, const bool plot_coverage) {
     int i, col;
     bool show_percentiles = false;
     const int n_rows = 10;
@@ -221,7 +223,7 @@ void print_hist(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *stats, in
     double hist_data[hist_size];
     double max_val = 0.0;
     for (i = 0; i < hist_size; ++i) {
-        hist_data[i] = 100 * hist[i] / (double) stats[tid].bin_width;
+        hist_data[i] = (plot_coverage?1:100) * hist[i] / (double) stats[tid].bin_width;
         if (hist_data[i] > max_val) max_val = hist_data[i];
     }
 
@@ -231,7 +233,9 @@ void print_hist(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *stats, in
     double row_bin_size = max_val / (double) n_rows;
     for (i = n_rows-1; i >= 0; --i) {
         double current_bin = row_bin_size * i;
-        if (show_percentiles) {
+        if (plot_coverage) {
+            fprintf(file_out, ">%8.1f ",i*row_bin_size);
+        } else if (show_percentiles) {
             fprintf(file_out, ">%3i%% ", i*10);
         } else {
             fprintf(file_out, ">%7.2f%% ", current_bin);
@@ -260,12 +264,18 @@ void print_hist(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *stats, in
             case 5: fprintf(file_out, "Mean coverage:   %.3gx",
                             stats[tid].summed_coverage / region_len); break;
             case 4: fprintf(file_out, "Mean baseQ:      %.3g",
-                            stats[tid].summed_baseQ/(double) stats[tid].summed_coverage); break;
+                            stats[tid].quality_bases > 0? stats[tid].summed_baseQ/(double) stats[tid].quality_bases : 0); break;
             case 3: fprintf(file_out, "Mean mapQ:       %.3g",
                             stats[tid].summed_mapQ/(double) stats[tid].n_selected_reads); break;
             case 1: fprintf(file_out, "Histo bin width: %sbp",
                             readable_bps(stats[tid].bin_width, buf)); break;
-            case 0: fprintf(file_out, "Histo max bin:   %.5g%%", max_val); break;
+            case 0: if (plot_coverage) {
+                        fprintf(file_out, "Histo max cov:   %.5g",  max_val);
+                    } else {
+                        fprintf(file_out, "Histo max bin:   %.5g%%", max_val);
+                    }
+                    break;
+
         };
         fputc('\n', file_out);
     }
@@ -306,6 +316,7 @@ int main_coverage(int argc, char *argv[]) {
     char **fn = NULL;
     int fail_flags = (BAM_FUNMAP | BAM_FSECONDARY | BAM_FQCFAIL | BAM_FDUP); // Default fail flags
     int required_flags = 0;
+    int print_value_warning = 0;
 
     int *n_plp = NULL;
     sam_hdr_t *h = NULL; // BAM header of the 1st input
@@ -313,6 +324,7 @@ int main_coverage(int argc, char *argv[]) {
     bool opt_print_header = true;
     bool opt_print_tabular = true;
     bool opt_print_histogram = false;
+    bool opt_plot_coverage = false;
     bool opt_full_utf = true;
 
     FILE *file_out = stdout;
@@ -332,6 +344,7 @@ int main_coverage(int argc, char *argv[]) {
         {"min-bq", required_argument, NULL, 'Q'},
         {"histogram", no_argument, NULL, 'm'},
         {"ascii", no_argument, NULL, 'A'},
+        {"plot-depth", no_argument, NULL, 'D'},
         {"output", required_argument, NULL, 'o'},
         {"no-header", no_argument, NULL, 'H'},
         {"n-bins", required_argument, NULL, 'w'},
@@ -344,7 +357,7 @@ int main_coverage(int argc, char *argv[]) {
     // parse the command line
     int c;
     opterr = 0;
-    while ((c = getopt_long(argc, argv, "Ao:l:q:Q:hHw:r:b:md:", lopts, NULL)) != -1) {
+    while ((c = getopt_long(argc, argv, "Ao:l:q:Q:hHw:r:b:md:D", lopts, NULL)) != -1) {
         switch (c) {
             case 1:
                 if ((required_flags = bam_str2flag(optarg)) < 0) {
@@ -368,6 +381,10 @@ int main_coverage(int argc, char *argv[]) {
             case 'A': opt_full_utf = false;
                       opt_print_histogram = true; opt_print_tabular = false;
                       break;
+            case 'D': opt_print_histogram = true;
+                     opt_print_tabular = false;
+                     opt_plot_coverage = true;
+                     break;
             case 'H': opt_print_header = false; break;
             case 'h': return usage();
             default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
@@ -555,12 +572,13 @@ int main_coverage(int argc, char *argv[]) {
         status = EXIT_FAILURE;
         goto coverage_end;
     }
+
     while ((ret=bam_mplp_auto(mplp, &tid, &pos, n_plp, plp)) > 0) { // come to the next covered position
 
         if (tid != old_tid) { // Next target sequence
             if (old_tid >= 0) {
                 if (opt_print_histogram) {
-                    print_hist(file_out, h, stats, old_tid, hist, n_bins, opt_full_utf);
+                    print_hist(file_out, h, stats, old_tid, hist, n_bins, opt_full_utf, opt_plot_coverage);
                     fputc('\n', file_out);
                 } else if (opt_print_tabular) {
                     print_tabular_line(file_out, h, stats, old_tid);
@@ -594,22 +612,32 @@ int main_coverage(int argc, char *argv[]) {
             for (j = 0; j < n_plp[i]; ++j) {
                 const bam_pileup1_t *p = plp[i] + j; // DON'T modify plp[][] unless you really know
 
-                if (p->is_del || p->is_refskip) --depth_at_pos; // having dels or refskips at tid:pos
-                else if (p->qpos < p->b->core.l_qseq &&
-                        bam_get_qual(p->b)[p->qpos] < opt_min_baseQ) --depth_at_pos; // low base quality
-                else
-                    stats[tid].summed_baseQ += bam_get_qual(p->b)[p->qpos];
+                if (p->is_del || p->is_refskip) {
+                    --depth_at_pos; // having dels or refskips at tid:pos
+                } else if (p->qpos < p->b->core.l_qseq) {
+                    if (bam_get_qual(p->b)[p->qpos] < opt_min_baseQ) {
+                        --depth_at_pos; // low base quality
+                    } else {
+                        stats[tid].summed_baseQ += bam_get_qual(p->b)[p->qpos];
+                        stats[tid].quality_bases++;
+                    }
+                } else {
+                    print_value_warning = 1; // no quality at position
+                }
             }
+
             if (depth_at_pos > 0) {
                 count_base = true;
                 stats[tid].summed_coverage += depth_at_pos;
             }
-            // hist[current_bin] += depth_at_pos;  // Add counts to the histogram here to have one based on coverage
-            //fprintf(file_out, "\t%d", n_plp[i] - m); // this the depth to output
+
+            if(current_bin < n_bins && opt_plot_coverage) {
+                hist[current_bin] += depth_at_pos;
+            }
         }
         if (count_base) {
             stats[tid].n_covered_bases++;
-            if (opt_print_histogram && current_bin < n_bins)
+            if (opt_print_histogram && current_bin < n_bins && !opt_plot_coverage)
                 ++(hist[current_bin]); // Histogram based on breadth of coverage
         }
     }
@@ -620,7 +648,7 @@ int main_coverage(int argc, char *argv[]) {
 
     if (tid < n_targets && tid >=0) {
         if (opt_print_histogram) {
-            print_hist(file_out, h, stats, tid, hist, n_bins, opt_full_utf);
+            print_hist(file_out, h, stats, tid, hist, n_bins, opt_full_utf, opt_plot_coverage);
         } else if (opt_print_tabular) {
             print_tabular_line(file_out, h, stats, tid);
         }
@@ -636,6 +664,10 @@ int main_coverage(int argc, char *argv[]) {
         }
     }
 
+    if (print_value_warning) {
+        print_error("coverage", "Warning:  Missing quality values in alignments.  Mean base quality calculated only on available values.");
+    }
+
     if (ret < 0) status = EXIT_FAILURE;
 
 coverage_end:
index 894f4ac68566054609fabaa2b747a666defd948f..bfd6ec935bc6768672be7d3e9cdeea95d36a54d1 100644 (file)
@@ -3,7 +3,7 @@
 /* coverage.c -- samtools coverage subcommand
 
     Copyright (C) 2018,2019 Florian Breitwieser
-    Portions copyright (C) 2019-2021 Genome Research Ltd.
+    Portions copyright (C) 2019-2021, 2023-2024 Genome Research Ltd.
 
     Author: Florian P Breitwieser <florian.bw@gmail.com>
 
@@ -62,6 +62,7 @@ typedef struct {  // auxiliary data structure to hold stats on coverage
     unsigned long long summed_coverage;
     unsigned long long summed_baseQ;
     unsigned long long summed_mapQ;
+    unsigned long long quality_bases;
     unsigned int n_reads;
     unsigned int n_selected_reads;
     bool covered;
@@ -107,7 +108,7 @@ static const char *const BLOCK_CHARS2[2] = {".", ":"};
 // in bam_plcmd.c
 int read_file_list(const char *file_list, int *n, char **argv[]);
 
-static int usage() {
+static int usage(void) {
     fprintf(samtools_stdout, "Usage: samtools coverage [options] in1.bam [in2.bam [...]]\n\n"
             "Input options:\n"
             "  -b, --bam-list FILE     list of input BAM filenames, one per line\n"
@@ -122,6 +123,7 @@ static int usage() {
             "                          effectively removing any depth limit.\n"
             "Output options:\n"
             "  -m, --histogram         show histogram instead of tabular output\n"
+            "  -D, --plot-depth        plot depth instead of tabular output\n"
             "  -A, --ascii             show only ASCII characters in histogram\n"
             "  -o, --output FILE       write output to FILE [samtools_stdout]\n"
             "  -H, --no-header         don't print a header in tabular mode\n"
@@ -205,13 +207,13 @@ void print_tabular_line(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *s
             stats[tid].n_covered_bases,
             100.0 * stats[tid].n_covered_bases / region_len,
             stats[tid].summed_coverage / region_len,
-            stats[tid].summed_coverage > 0? stats[tid].summed_baseQ/(double) stats[tid].summed_coverage : 0,
+            stats[tid].quality_bases > 0? stats[tid].summed_baseQ/(double) stats[tid].quality_bases : 0,
             stats[tid].n_selected_reads > 0? stats[tid].summed_mapQ/(double) stats[tid].n_selected_reads : 0
            );
 }
 
 void print_hist(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *stats, int tid, const uint32_t *hist,
-        const int hist_size, const bool full_utf) {
+        const int hist_size, const bool full_utf, const bool plot_coverage) {
     int i, col;
     bool show_percentiles = false;
     const int n_rows = 10;
@@ -223,7 +225,7 @@ void print_hist(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *stats, in
     double hist_data[hist_size];
     double max_val = 0.0;
     for (i = 0; i < hist_size; ++i) {
-        hist_data[i] = 100 * hist[i] / (double) stats[tid].bin_width;
+        hist_data[i] = (plot_coverage?1:100) * hist[i] / (double) stats[tid].bin_width;
         if (hist_data[i] > max_val) max_val = hist_data[i];
     }
 
@@ -233,7 +235,9 @@ void print_hist(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *stats, in
     double row_bin_size = max_val / (double) n_rows;
     for (i = n_rows-1; i >= 0; --i) {
         double current_bin = row_bin_size * i;
-        if (show_percentiles) {
+        if (plot_coverage) {
+            fprintf(file_out, ">%8.1f ",i*row_bin_size);
+        } else if (show_percentiles) {
             fprintf(file_out, ">%3i%% ", i*10);
         } else {
             fprintf(file_out, ">%7.2f%% ", current_bin);
@@ -262,12 +266,18 @@ void print_hist(FILE *file_out, const sam_hdr_t *h, const stats_aux_t *stats, in
             case 5: fprintf(file_out, "Mean coverage:   %.3gx",
                             stats[tid].summed_coverage / region_len); break;
             case 4: fprintf(file_out, "Mean baseQ:      %.3g",
-                            stats[tid].summed_baseQ/(double) stats[tid].summed_coverage); break;
+                            stats[tid].quality_bases > 0? stats[tid].summed_baseQ/(double) stats[tid].quality_bases : 0); break;
             case 3: fprintf(file_out, "Mean mapQ:       %.3g",
                             stats[tid].summed_mapQ/(double) stats[tid].n_selected_reads); break;
             case 1: fprintf(file_out, "Histo bin width: %sbp",
                             readable_bps(stats[tid].bin_width, buf)); break;
-            case 0: fprintf(file_out, "Histo max bin:   %.5g%%", max_val); break;
+            case 0: if (plot_coverage) {
+                        fprintf(file_out, "Histo max cov:   %.5g",  max_val);
+                    } else {
+                        fprintf(file_out, "Histo max bin:   %.5g%%", max_val);
+                    }
+                    break;
+
         };
         fputc('\n', file_out);
     }
@@ -308,6 +318,7 @@ int main_coverage(int argc, char *argv[]) {
     char **fn = NULL;
     int fail_flags = (BAM_FUNMAP | BAM_FSECONDARY | BAM_FQCFAIL | BAM_FDUP); // Default fail flags
     int required_flags = 0;
+    int print_value_warning = 0;
 
     int *n_plp = NULL;
     sam_hdr_t *h = NULL; // BAM header of the 1st input
@@ -315,6 +326,7 @@ int main_coverage(int argc, char *argv[]) {
     bool opt_print_header = true;
     bool opt_print_tabular = true;
     bool opt_print_histogram = false;
+    bool opt_plot_coverage = false;
     bool opt_full_utf = true;
 
     FILE *file_out = samtools_stdout;
@@ -334,6 +346,7 @@ int main_coverage(int argc, char *argv[]) {
         {"min-bq", required_argument, NULL, 'Q'},
         {"histogram", no_argument, NULL, 'm'},
         {"ascii", no_argument, NULL, 'A'},
+        {"plot-depth", no_argument, NULL, 'D'},
         {"output", required_argument, NULL, 'o'},
         {"no-header", no_argument, NULL, 'H'},
         {"n-bins", required_argument, NULL, 'w'},
@@ -346,7 +359,7 @@ int main_coverage(int argc, char *argv[]) {
     // parse the command line
     int c;
     opterr = 0;
-    while ((c = getopt_long(argc, argv, "Ao:l:q:Q:hHw:r:b:md:", lopts, NULL)) != -1) {
+    while ((c = getopt_long(argc, argv, "Ao:l:q:Q:hHw:r:b:md:D", lopts, NULL)) != -1) {
         switch (c) {
             case 1:
                 if ((required_flags = bam_str2flag(optarg)) < 0) {
@@ -370,6 +383,10 @@ int main_coverage(int argc, char *argv[]) {
             case 'A': opt_full_utf = false;
                       opt_print_histogram = true; opt_print_tabular = false;
                       break;
+            case 'D': opt_print_histogram = true;
+                     opt_print_tabular = false;
+                     opt_plot_coverage = true;
+                     break;
             case 'H': opt_print_header = false; break;
             case 'h': return usage();
             default:  if (parse_sam_global_opt(c, optarg, lopts, &ga) == 0) break;
@@ -557,12 +574,13 @@ int main_coverage(int argc, char *argv[]) {
         status = EXIT_FAILURE;
         goto coverage_end;
     }
+
     while ((ret=bam_mplp_auto(mplp, &tid, &pos, n_plp, plp)) > 0) { // come to the next covered position
 
         if (tid != old_tid) { // Next target sequence
             if (old_tid >= 0) {
                 if (opt_print_histogram) {
-                    print_hist(file_out, h, stats, old_tid, hist, n_bins, opt_full_utf);
+                    print_hist(file_out, h, stats, old_tid, hist, n_bins, opt_full_utf, opt_plot_coverage);
                     fputc('\n', file_out);
                 } else if (opt_print_tabular) {
                     print_tabular_line(file_out, h, stats, old_tid);
@@ -596,22 +614,32 @@ int main_coverage(int argc, char *argv[]) {
             for (j = 0; j < n_plp[i]; ++j) {
                 const bam_pileup1_t *p = plp[i] + j; // DON'T modify plp[][] unless you really know
 
-                if (p->is_del || p->is_refskip) --depth_at_pos; // having dels or refskips at tid:pos
-                else if (p->qpos < p->b->core.l_qseq &&
-                        bam_get_qual(p->b)[p->qpos] < opt_min_baseQ) --depth_at_pos; // low base quality
-                else
-                    stats[tid].summed_baseQ += bam_get_qual(p->b)[p->qpos];
+                if (p->is_del || p->is_refskip) {
+                    --depth_at_pos; // having dels or refskips at tid:pos
+                } else if (p->qpos < p->b->core.l_qseq) {
+                    if (bam_get_qual(p->b)[p->qpos] < opt_min_baseQ) {
+                        --depth_at_pos; // low base quality
+                    } else {
+                        stats[tid].summed_baseQ += bam_get_qual(p->b)[p->qpos];
+                        stats[tid].quality_bases++;
+                    }
+                } else {
+                    print_value_warning = 1; // no quality at position
+                }
             }
+
             if (depth_at_pos > 0) {
                 count_base = true;
                 stats[tid].summed_coverage += depth_at_pos;
             }
-            // hist[current_bin] += depth_at_pos;  // Add counts to the histogram here to have one based on coverage
-            //fprintf(file_out, "\t%d", n_plp[i] - m); // this the depth to output
+
+            if(current_bin < n_bins && opt_plot_coverage) {
+                hist[current_bin] += depth_at_pos;
+            }
         }
         if (count_base) {
             stats[tid].n_covered_bases++;
-            if (opt_print_histogram && current_bin < n_bins)
+            if (opt_print_histogram && current_bin < n_bins && !opt_plot_coverage)
                 ++(hist[current_bin]); // Histogram based on breadth of coverage
         }
     }
@@ -622,7 +650,7 @@ int main_coverage(int argc, char *argv[]) {
 
     if (tid < n_targets && tid >=0) {
         if (opt_print_histogram) {
-            print_hist(file_out, h, stats, tid, hist, n_bins, opt_full_utf);
+            print_hist(file_out, h, stats, tid, hist, n_bins, opt_full_utf, opt_plot_coverage);
         } else if (opt_print_tabular) {
             print_tabular_line(file_out, h, stats, tid);
         }
@@ -638,6 +666,10 @@ int main_coverage(int argc, char *argv[]) {
         }
     }
 
+    if (print_value_warning) {
+        print_error("coverage", "Warning:  Missing quality values in alignments.  Mean base quality calculated only on available values.");
+    }
+
     if (ret < 0) status = EXIT_FAILURE;
 
 coverage_end:
index 63204d1460f786aaa07e467b88244a19a44b73e9..616d82529f71bbedd27e8491ec41afcc52d33f32 100644 (file)
@@ -1,6 +1,6 @@
 /*  faidx.c -- faidx subcommand.
 
-    Copyright (C) 2008, 2009, 2013, 2016, 2018-2020, 2022 Genome Research Ltd.
+    Copyright (C) 2008, 2009, 2013, 2016, 2018-2020, 2022, 2024 Genome Research Ltd.
     Portions copyright (C) 2011 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -44,6 +44,9 @@ History:
 #include <htslib/hts.h>
 #include <htslib/hfile.h>
 #include <htslib/kstring.h>
+#include <htslib/bgzf.h>
+#include <htslib/thread_pool.h>
+#include "sam_opts.h"
 #include "samtools.h"
 
 // Negative indicates the same as input data
@@ -53,6 +56,15 @@ History:
 #   define ABS(x) ((x)>=0?(x):-(x))
 #endif
 
+//new params required for output creation
+typedef struct output {
+    int isbgzip;                //is bgzip or uncompressed file
+    FILE *fp;                   //uncompressed file pointer
+    BGZF *bgzf_fp;              //bgzf file pointer
+    sam_global_args *gopt;      //options
+    kstring_t buffer;
+} output;
+
 static unsigned char comp_base[256] = {
   0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,  15,
  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,  26,  27,  28,  29,  30,  31,
@@ -98,8 +110,19 @@ static void reverse(char *str, const hts_pos_t len) {
     }
 }
 
+/// wrappedwrite  - wraps the fwrite and bgzf_write
+/** @param out    - pointer to data required to write output
+*   @param buffer - data to write
+*   @param length - data length
+* returns error or length written on success
+*/
+static inline size_t wrappedwrite(output *out, const char *buffer, size_t length)
+{
+    return out->isbgzip ? bgzf_write(out->bgzf_fp, buffer, length) :
+        fwrite(buffer, 1, length, out->fp);
+}
 
-static int write_line(faidx_t *faid, FILE *file, const char *line, const char *name,
+static int write_line(faidx_t *faid, output *out, const char *line, const char *name,
                       const int ignore, const hts_pos_t length, const hts_pos_t seq_len) {
     int id;
     hts_pos_t beg, end;
@@ -124,8 +147,8 @@ static int write_line(faidx_t *faid, FILE *file, const char *line, const char *n
     for (i = 0; i < seq_sz; i += length)
     {
         hts_pos_t len = i + length < seq_sz ? length : seq_sz - i;
-        if (fwrite(line + i, 1, len, file) < len ||
-            fputc('\n', file) == EOF) {
+        if (wrappedwrite(out, line + i, len) < len ||
+              wrappedwrite(out, "\n", 1) < 1) {
             print_error_errno("faidx", "failed to write output");
             return EXIT_FAILURE;
         }
@@ -135,58 +158,66 @@ static int write_line(faidx_t *faid, FILE *file, const char *line, const char *n
 }
 
 
-static int write_output(faidx_t *faid, FILE *file, const char *name, const int ignore,
+static int write_output(faidx_t *faid, output *out, const char *name, const int ignore,
                         const hts_pos_t length, const int rev,
                         const char *pos_strand_name, const char *neg_strand_name,
                         enum fai_format_options format) {
-    hts_pos_t seq_len, wrap_len = length;
+    hts_pos_t seq_len, wrap_len = length, len = 0;
+    char *seq =  NULL, *qual = NULL;
+    int ret = EXIT_FAILURE;
+
     if (wrap_len < 0)
         wrap_len = fai_line_length(faid, name);
     if (wrap_len <= 0)
         wrap_len = HTS_POS_MAX;
-    char *seq = fai_fetch64(faid, name, &seq_len);
-
-    if (format == FAI_FASTA) {
-        fprintf(file, ">%s%s\n", name, rev ? neg_strand_name : pos_strand_name);
-    } else {
-        fprintf(file, "@%s%s\n", name, rev ? neg_strand_name : pos_strand_name);
-    }
 
+    seq = fai_fetch64(faid, name, &seq_len);
     if (rev && seq_len > 0) {
         reverse_complement(seq, seq_len);
     }
-
-    if (write_line(faid, file, seq, name, ignore, wrap_len, seq_len)
-        == EXIT_FAILURE) {
-        free(seq);
-        return EXIT_FAILURE;
+    //write the name
+    len = ksprintf(&out->buffer, "%c%s%s\n", format == FAI_FASTA ? '>' : '@', name, rev ? neg_strand_name : pos_strand_name);
+    if (wrappedwrite(out, out->buffer.s, out->buffer.l) < len) {
+        fprintf(stderr,"[faidx] Failed to write buffer\n");
+        goto exit;
+    }
+    ks_clear(&out->buffer);
+    //write bases
+    if ((ret = write_line(faid, out, seq, name, ignore, wrap_len, seq_len) == EXIT_FAILURE)) {
+        goto exit;
     }
-
-    free(seq);
 
     if (format == FAI_FASTQ) {
-        fprintf(file, "+\n");
-
-        char *qual = fai_fetchqual64(faid, name, &seq_len);
-
+        //write quality
+        qual = fai_fetchqual64(faid, name, &seq_len);
         if (rev && seq_len > 0) {
             reverse(qual, seq_len);
         }
 
-        if (write_line(faid, file, qual, name, ignore, wrap_len, seq_len)
-            == EXIT_FAILURE) {
-            free(qual);
-            return EXIT_FAILURE;
+        len = ksprintf(&out->buffer, "+\n");
+        if (wrappedwrite(out, out->buffer.s, out->buffer.l) < len) {
+            fprintf(stderr,"[faidx] Failed to write buffer\n");
+            goto exit;
         }
+        ks_clear(&out->buffer);
+        if ((ret = write_line(faid, out, qual, name, ignore, wrap_len, seq_len) == EXIT_FAILURE)) {
+            goto exit;
+        }
+    }
+    ret = EXIT_SUCCESS;
 
+exit:
+    if (seq) {
+        free(seq);
+    }
+    if (qual) {
         free(qual);
     }
-
-    return EXIT_SUCCESS;
+    return ret;
 }
 
 
-static int read_regions_from_file(faidx_t *faid, hFILE *in_file, FILE *file, const int ignore,
+static int read_regions_from_file(faidx_t *faid, hFILE *in_file, output *out, const int ignore,
                                   const hts_pos_t length, const int rev,
                                   const char *pos_strand_name,
                                   const char *neg_strand_name,
@@ -195,7 +226,7 @@ static int read_regions_from_file(faidx_t *faid, hFILE *in_file, FILE *file, con
     int ret = EXIT_FAILURE;
 
     while (line.l = 0, kgetline(&line, (kgets_func *)hgets, in_file) >= 0) {
-        if ((ret = write_output(faid, file, line.s, ignore, length, rev, pos_strand_name, neg_strand_name, format)) == EXIT_FAILURE) {
+        if ((ret = write_output(faid, out, line.s, ignore, length, rev, pos_strand_name, neg_strand_name, format)) == EXIT_FAILURE) {
             break;
         }
     }
@@ -221,26 +252,27 @@ static int usage(FILE *fp, enum fai_format_options format, int exit_status)
 
     fprintf(fp, "Usage: samtools %s [<reg> [...]]\n", tool);
     fprintf(fp, "Option: \n"
-                " -o, --output FILE        Write %s to file.\n"
-                " -n, --length INT         Length of %s sequence line. [60]\n"
-                " -c, --continue           Continue after trying to retrieve missing region.\n"
-                " -r, --region-file FILE   File of regions.  Format is chr:from-to. One per line.\n"
-                " -i, --reverse-complement Reverse complement sequences.\n"
-                "     --mark-strand TYPE   Add strand indicator to sequence name\n"
-                "                          TYPE = rc   for /rc on negative strand (default)\n"
-                "                                 no   for no strand indicator\n"
-                "                                 sign for (+) / (-)\n"
-                "                                 custom,<pos>,<neg> for custom indicator\n"
-                "     --fai-idx      FILE  name of the index file (default %s.fai).\n"
-                "     --gzi-idx      FILE  name of compressed file index (default %s.gz.gzi).\n",
+                "  -o, --output FILE        Write %s to file.\n"
+                "  -n, --length INT         Length of %s sequence line. [60]\n"
+                "  -c, --continue           Continue after trying to retrieve missing region.\n"
+                "  -r, --region-file FILE   File of regions.  Format is chr:from-to. One per line.\n"
+                "  -i, --reverse-complement Reverse complement sequences.\n"
+                "      --mark-strand TYPE   Add strand indicator to sequence name\n"
+                "                           TYPE = rc   for /rc on negative strand (default)\n"
+                "                                  no   for no strand indicator\n"
+                "                                  sign for (+) / (-)\n"
+                "                                  custom,<pos>,<neg> for custom indicator\n"
+                "      --fai-idx      FILE  name of the index file (default %s.fai).\n"
+                "      --gzi-idx      FILE  name of compressed file index (default %s.gz.gzi).\n",
                 file_type, file_type, index_name, index_name);
 
 
     if (format == FAI_FASTA) {
-       fprintf(fp, " -f, --fastq              File and index in FASTQ format.\n");
+       fprintf(fp, "  -f, --fastq              File and index in FASTQ format.\n");
     }
 
-    fprintf(fp, " -h, --help               This message.\n");
+    fprintf(fp, "  -h, --help               This message.\n");
+    sam_global_opt_help(fp, "---.-@--");
 
     return exit_status;
 }
@@ -256,9 +288,14 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
     char *strand_names = NULL; // Used for custom strand annotation
     char *fai_name = NULL; // specified index name
     char *gzi_name = NULL; // specified compressed index name
-    FILE* file_out = stdout;/* output stream */
+    sam_global_args ga = SAM_GLOBAL_ARGS_INIT;
+    int exit_status = EXIT_FAILURE, flushed = 0;
+    struct output out = { 0, stdout, NULL, &ga, KS_INITIALIZE}; //data required for output writing
+    faidx_t *fai = NULL;
+    hts_tpool *pool = NULL;
 
     static const struct option lopts[] = {
+        SAM_OPT_GLOBAL_OPTIONS('-', '-', '-', 0, '-', '@'),     //output format opt and thread count - long options
         { "output", required_argument,       NULL, 'o' },
         { "help",   no_argument,             NULL, 'h' },
         { "length", required_argument,       NULL, 'n' },
@@ -272,9 +309,20 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
         { NULL, 0, NULL, 0 }
     };
 
-    while ((c = getopt_long(argc, argv, "ho:n:cr:fi", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "ho:n:cr:fi@:", lopts, NULL)) >= 0) {
         switch (c) {
-            case 'o': output_file = optarg; break;
+            case 'o':
+                {
+                    output_file = optarg;
+                    char *ext = strrchr(output_file, '.');
+                    if (!ext) {
+                        break;
+                    }
+                    if (!strcmp(ext, ".gz") || !strcmp(ext, ".bgz") || !strcmp(ext, ".bgzf")) {
+                        out.isbgzip = 1;        //bgzip output
+                    }
+                    break;
+                }
             case 'n': line_len = strtol(optarg, NULL, 10);
                       if (line_len < 0) {
                         fprintf(stderr,"[faidx] bad line length '%s', using default:%d\n",optarg,ABS(DEFAULT_FASTA_LINE_LEN));
@@ -285,8 +333,14 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
             case 'r': region_file = optarg; break;
             case 'f': format = FAI_FASTQ; break;
             case 'i': rev = 1; break;
-            case '?': return usage(stderr, format, EXIT_FAILURE);
-            case 'h': return usage(stdout, format, EXIT_SUCCESS);
+            case '?':
+                exit_status = usage(stderr, format, EXIT_FAILURE);
+                goto exit2;
+                break;
+            case 'h':
+                exit_status = usage(stdout, format, EXIT_SUCCESS);
+                goto exit2;
+                break;
             case 1000:
                 if (strcmp(optarg, "no") == 0) {
                     pos_strand_name = neg_strand_name = "";
@@ -303,7 +357,7 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
                     strand_names = pos_strand_name = malloc(len + 2);
                     if (!strand_names) {
                         fprintf(stderr, "[faidx] Out of memory\n");
-                        return EXIT_FAILURE;
+                        goto exit2;
                     }
                     neg_strand_name = pos_strand_name + comma + 1;
                     memcpy(pos_strand_name, optarg + 7, comma);
@@ -314,17 +368,26 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
                     neg_strand_name[len - comma] = '\0';
                 } else {
                     fprintf(stderr, "[faidx] Unknown --mark-strand option \"%s\"\n", optarg);
-                    return usage(stderr, format, EXIT_FAILURE);
+                    exit_status = usage(stderr, format, EXIT_FAILURE);
+                    goto exit2;
                 }
                 break;
             case 1001: fai_name = optarg; break;
             case 1002: gzi_name = optarg; break;
-            default:  break;
+            // handle standard samtools options like thread count, compression level...
+            default:
+                if (parse_sam_global_opt(c, optarg, lopts, &ga)) {
+                    fprintf(stderr, "[faidx] Invalid option \"%s\"\n", optarg);
+                    goto exit2;
+                }
+                break;
         }
     }
 
-    if ( argc==optind )
-        return usage(stdout, format, EXIT_SUCCESS);
+    if ( argc==optind ) {
+        exit_status = usage(stdout, format, EXIT_SUCCESS);
+        goto exit2;
+    }
 
     if (optind+1 == argc && !region_file) {
         if (output_file && !fai_name)
@@ -341,73 +404,127 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
             else
                 fprintf(stderr, "\n");
 
-            return EXIT_FAILURE;
+            goto exit2;
         }
+        exit_status = EXIT_SUCCESS;
+        goto exit2;
+    }
 
-        return 0;
+    if (out.gopt->nthreads > 0) {       //setup thread pool
+        if (!(pool = hts_tpool_init(out.gopt->nthreads))) {
+            fprintf(stderr, "Failed to setup thread pool\n");
+        }
     }
 
-    faidx_t *fai = fai_load3_format(argv[optind], fai_name, gzi_name, FAI_CREATE, format);
+    fai = fai_load3_format(argv[optind], fai_name, gzi_name, FAI_CREATE, format);
 
     if (!fai) {
         if (fai_name)
             fprintf(stderr, "[faidx] Could not load fai index %s", fai_name);
         else
-            fprintf(stderr, "[faidx] Could not build fai index %s.fai", argv[optind]);
+            fprintf(stderr, "[faidx] Could not load fai index %s.fai", argv[optind]);
 
         if (gzi_name)
             fprintf(stderr, " or compressed index %s\n", gzi_name);
         else
             fprintf(stderr, "\n");
 
-        return EXIT_FAILURE;
+        goto exit2;
+    }
+
+    if (pool) {                         //use thread pool if set
+        if (fai_thread_pool(fai, pool, 0)) {
+            fprintf(stderr, "Failed to set thread pool for reading\n");
+        }
     }
 
     /** output file provided by user */
     if( output_file != NULL ) {
         if( strcmp( output_file, argv[optind] ) == 0 ) {
             fprintf(stderr,"[faidx] Same input/output : %s\n", output_file);
-            return EXIT_FAILURE;
+            goto exit2;
+        }
+        if (!out.isbgzip) {
+            out.fp = fopen( output_file, "w" );
+        } else {
+            hts_opt *opts = (hts_opt *)(out.gopt->out.specific);
+            char mode[13] = "w";
+            int level = 4;                                      //default compression level
+            while (opts) {
+                if (opts->opt == HTS_OPT_COMPRESSION_LEVEL) {   //compression level
+                    level = opts->val.i;
+                    break;
+                }
+                opts = opts->next;
+            }
+            if (level >= 0) {
+                snprintf(mode, sizeof(mode), "w%d", level);     //pass compression with mode
+            }
+            out.bgzf_fp = bgzf_open(output_file, mode);
         }
 
-        file_out = fopen( output_file, "w" );
-
-        if( file_out == NULL) {
+        if( (!out.isbgzip && out.fp == NULL) || (out.isbgzip && out.bgzf_fp == NULL)) {
             fprintf(stderr,"[faidx] Cannot open \"%s\" for writing :%s.\n", output_file, strerror(errno) );
-            return EXIT_FAILURE;
+            goto exit2;
+        }
+        if (out.isbgzip && pool) {                              //use thread pool if set
+            if (bgzf_thread_pool(out.bgzf_fp, pool, 0)) {
+                fprintf(stderr, "Failed to set thread pool for writing\n");
+            }
         }
     }
 
-    int exit_status = EXIT_SUCCESS;
-
     if (region_file) {
         hFILE *rf;
 
         if ((rf = hopen(region_file, "r"))) {
-            exit_status = read_regions_from_file(fai, rf, file_out, ignore_error, line_len, rev, pos_strand_name, neg_strand_name, format);
+            exit_status = read_regions_from_file(fai, rf, &out, ignore_error, line_len, rev, pos_strand_name, neg_strand_name, format);
 
             if (hclose(rf) != 0) {
                 fprintf(stderr, "[faidx] Warning: failed to close %s", region_file);
             }
+            if (exit_status == EXIT_FAILURE) {
+                goto exit1;
+            }
         } else {
             fprintf(stderr, "[faidx] Failed to open \"%s\" for reading.\n", region_file);
-            exit_status = EXIT_FAILURE;
+            goto exit1;
         }
     }
 
+    exit_status = EXIT_SUCCESS;
     while ( ++optind<argc && exit_status == EXIT_SUCCESS) {
-        exit_status = write_output(fai, file_out, argv[optind], ignore_error, line_len, rev, pos_strand_name, neg_strand_name, format);
+        exit_status = write_output(fai, &out, argv[optind], ignore_error, line_len, rev, pos_strand_name, neg_strand_name, format);
     }
 
-    fai_destroy(fai);
-
-    if (fflush(file_out) == EOF) {
-        print_error_errno("faidx", "failed to flush output");
+    flushed = out.isbgzip ? bgzf_flush(out.bgzf_fp) : fflush(out.fp);
+    if (flushed == EOF) {
+        print_error_errno("faidx", "Failed to flush output\n");
         exit_status = EXIT_FAILURE;
     }
 
-    if( output_file != NULL) fclose(file_out);
-    free(strand_names);
+exit1:
+    if( output_file != NULL && !out.isbgzip) {
+        fclose(out.fp);     //no need to check result as already flushed
+    } else if( output_file != NULL && out.isbgzip) {
+        if (bgzf_close(out.bgzf_fp) < 0) {
+            print_error_errno("faidx", "Failed to close output\n");
+            exit_status = EXIT_FAILURE;
+        }
+    }
+
+exit2:
+    if (strand_names) {
+        free(strand_names);
+    }
+    if (fai) {
+        fai_destroy(fai);
+    }
+    if (pool) {
+        hts_tpool_destroy(pool);
+    }
+    sam_global_args_free(&ga);
+    ks_free(&out.buffer);
 
     return exit_status;
 }
index 6160661eab72cd0e1bb19aafbef0a500ce5f9fca..f40209b41ab46ed4b87f43fefbf097bb5318d974 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  faidx.c -- faidx subcommand.
 
-    Copyright (C) 2008, 2009, 2013, 2016, 2018-2020, 2022 Genome Research Ltd.
+    Copyright (C) 2008, 2009, 2013, 2016, 2018-2020, 2022, 2024 Genome Research Ltd.
     Portions copyright (C) 2011 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -46,6 +46,9 @@ History:
 #include <htslib/hts.h>
 #include <htslib/hfile.h>
 #include <htslib/kstring.h>
+#include <htslib/bgzf.h>
+#include <htslib/thread_pool.h>
+#include "sam_opts.h"
 #include "samtools.h"
 
 // Negative indicates the same as input data
@@ -55,6 +58,15 @@ History:
 #   define ABS(x) ((x)>=0?(x):-(x))
 #endif
 
+//new params required for output creation
+typedef struct output {
+    int isbgzip;                //is bgzip or uncompressed file
+    FILE *fp;                   //uncompressed file pointer
+    BGZF *bgzf_fp;              //bgzf file pointer
+    sam_global_args *gopt;      //options
+    kstring_t buffer;
+} output;
+
 static unsigned char comp_base[256] = {
   0,   1,   2,   3,   4,   5,   6,   7,   8,   9,  10,  11,  12,  13,  14,  15,
  16,  17,  18,  19,  20,  21,  22,  23,  24,  25,  26,  27,  28,  29,  30,  31,
@@ -100,8 +112,19 @@ static void reverse(char *str, const hts_pos_t len) {
     }
 }
 
+/// wrappedwrite  - wraps the fwrite and bgzf_write
+/** @param out    - pointer to data required to write output
+*   @param buffer - data to write
+*   @param length - data length
+* returns error or length written on success
+*/
+static inline size_t wrappedwrite(output *out, const char *buffer, size_t length)
+{
+    return out->isbgzip ? bgzf_write(out->bgzf_fp, buffer, length) :
+        fwrite(buffer, 1, length, out->fp);
+}
 
-static int write_line(faidx_t *faid, FILE *file, const char *line, const char *name,
+static int write_line(faidx_t *faid, output *out, const char *line, const char *name,
                       const int ignore, const hts_pos_t length, const hts_pos_t seq_len) {
     int id;
     hts_pos_t beg, end;
@@ -126,8 +149,8 @@ static int write_line(faidx_t *faid, FILE *file, const char *line, const char *n
     for (i = 0; i < seq_sz; i += length)
     {
         hts_pos_t len = i + length < seq_sz ? length : seq_sz - i;
-        if (fwrite(line + i, 1, len, file) < len ||
-            fputc('\n', file) == EOF) {
+        if (wrappedwrite(out, line + i, len) < len ||
+              wrappedwrite(out, "\n", 1) < 1) {
             print_error_errno("faidx", "failed to write output");
             return EXIT_FAILURE;
         }
@@ -137,58 +160,66 @@ static int write_line(faidx_t *faid, FILE *file, const char *line, const char *n
 }
 
 
-static int write_output(faidx_t *faid, FILE *file, const char *name, const int ignore,
+static int write_output(faidx_t *faid, output *out, const char *name, const int ignore,
                         const hts_pos_t length, const int rev,
                         const char *pos_strand_name, const char *neg_strand_name,
                         enum fai_format_options format) {
-    hts_pos_t seq_len, wrap_len = length;
+    hts_pos_t seq_len, wrap_len = length, len = 0;
+    char *seq =  NULL, *qual = NULL;
+    int ret = EXIT_FAILURE;
+
     if (wrap_len < 0)
         wrap_len = fai_line_length(faid, name);
     if (wrap_len <= 0)
         wrap_len = HTS_POS_MAX;
-    char *seq = fai_fetch64(faid, name, &seq_len);
-
-    if (format == FAI_FASTA) {
-        fprintf(file, ">%s%s\n", name, rev ? neg_strand_name : pos_strand_name);
-    } else {
-        fprintf(file, "@%s%s\n", name, rev ? neg_strand_name : pos_strand_name);
-    }
 
+    seq = fai_fetch64(faid, name, &seq_len);
     if (rev && seq_len > 0) {
         reverse_complement(seq, seq_len);
     }
-
-    if (write_line(faid, file, seq, name, ignore, wrap_len, seq_len)
-        == EXIT_FAILURE) {
-        free(seq);
-        return EXIT_FAILURE;
+    //write the name
+    len = ksprintf(&out->buffer, "%c%s%s\n", format == FAI_FASTA ? '>' : '@', name, rev ? neg_strand_name : pos_strand_name);
+    if (wrappedwrite(out, out->buffer.s, out->buffer.l) < len) {
+        fprintf(samtools_stderr,"[faidx] Failed to write buffer\n");
+        goto exit;
+    }
+    ks_clear(&out->buffer);
+    //write bases
+    if ((ret = write_line(faid, out, seq, name, ignore, wrap_len, seq_len) == EXIT_FAILURE)) {
+        goto exit;
     }
-
-    free(seq);
 
     if (format == FAI_FASTQ) {
-        fprintf(file, "+\n");
-
-        char *qual = fai_fetchqual64(faid, name, &seq_len);
-
+        //write quality
+        qual = fai_fetchqual64(faid, name, &seq_len);
         if (rev && seq_len > 0) {
             reverse(qual, seq_len);
         }
 
-        if (write_line(faid, file, qual, name, ignore, wrap_len, seq_len)
-            == EXIT_FAILURE) {
-            free(qual);
-            return EXIT_FAILURE;
+        len = ksprintf(&out->buffer, "+\n");
+        if (wrappedwrite(out, out->buffer.s, out->buffer.l) < len) {
+            fprintf(samtools_stderr,"[faidx] Failed to write buffer\n");
+            goto exit;
         }
+        ks_clear(&out->buffer);
+        if ((ret = write_line(faid, out, qual, name, ignore, wrap_len, seq_len) == EXIT_FAILURE)) {
+            goto exit;
+        }
+    }
+    ret = EXIT_SUCCESS;
 
+exit:
+    if (seq) {
+        free(seq);
+    }
+    if (qual) {
         free(qual);
     }
-
-    return EXIT_SUCCESS;
+    return ret;
 }
 
 
-static int read_regions_from_file(faidx_t *faid, hFILE *in_file, FILE *file, const int ignore,
+static int read_regions_from_file(faidx_t *faid, hFILE *in_file, output *out, const int ignore,
                                   const hts_pos_t length, const int rev,
                                   const char *pos_strand_name,
                                   const char *neg_strand_name,
@@ -197,7 +228,7 @@ static int read_regions_from_file(faidx_t *faid, hFILE *in_file, FILE *file, con
     int ret = EXIT_FAILURE;
 
     while (line.l = 0, kgetline(&line, (kgets_func *)hgets, in_file) >= 0) {
-        if ((ret = write_output(faid, file, line.s, ignore, length, rev, pos_strand_name, neg_strand_name, format)) == EXIT_FAILURE) {
+        if ((ret = write_output(faid, out, line.s, ignore, length, rev, pos_strand_name, neg_strand_name, format)) == EXIT_FAILURE) {
             break;
         }
     }
@@ -223,26 +254,27 @@ static int usage(FILE *fp, enum fai_format_options format, int exit_status)
 
     fprintf(fp, "Usage: samtools %s [<reg> [...]]\n", tool);
     fprintf(fp, "Option: \n"
-                " -o, --output FILE        Write %s to file.\n"
-                " -n, --length INT         Length of %s sequence line. [60]\n"
-                " -c, --continue           Continue after trying to retrieve missing region.\n"
-                " -r, --region-file FILE   File of regions.  Format is chr:from-to. One per line.\n"
-                " -i, --reverse-complement Reverse complement sequences.\n"
-                "     --mark-strand TYPE   Add strand indicator to sequence name\n"
-                "                          TYPE = rc   for /rc on negative strand (default)\n"
-                "                                 no   for no strand indicator\n"
-                "                                 sign for (+) / (-)\n"
-                "                                 custom,<pos>,<neg> for custom indicator\n"
-                "     --fai-idx      FILE  name of the index file (default %s.fai).\n"
-                "     --gzi-idx      FILE  name of compressed file index (default %s.gz.gzi).\n",
+                "  -o, --output FILE        Write %s to file.\n"
+                "  -n, --length INT         Length of %s sequence line. [60]\n"
+                "  -c, --continue           Continue after trying to retrieve missing region.\n"
+                "  -r, --region-file FILE   File of regions.  Format is chr:from-to. One per line.\n"
+                "  -i, --reverse-complement Reverse complement sequences.\n"
+                "      --mark-strand TYPE   Add strand indicator to sequence name\n"
+                "                           TYPE = rc   for /rc on negative strand (default)\n"
+                "                                  no   for no strand indicator\n"
+                "                                  sign for (+) / (-)\n"
+                "                                  custom,<pos>,<neg> for custom indicator\n"
+                "      --fai-idx      FILE  name of the index file (default %s.fai).\n"
+                "      --gzi-idx      FILE  name of compressed file index (default %s.gz.gzi).\n",
                 file_type, file_type, index_name, index_name);
 
 
     if (format == FAI_FASTA) {
-       fprintf(fp, " -f, --fastq              File and index in FASTQ format.\n");
+       fprintf(fp, "  -f, --fastq              File and index in FASTQ format.\n");
     }
 
-    fprintf(fp, " -h, --help               This message.\n");
+    fprintf(fp, "  -h, --help               This message.\n");
+    sam_global_opt_help(fp, "---.-@--");
 
     return exit_status;
 }
@@ -258,9 +290,14 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
     char *strand_names = NULL; // Used for custom strand annotation
     char *fai_name = NULL; // specified index name
     char *gzi_name = NULL; // specified compressed index name
-    FILE* file_out = samtools_stdout;/* output stream */
+    sam_global_args ga = SAM_GLOBAL_ARGS_INIT;
+    int exit_status = EXIT_FAILURE, flushed = 0;
+    struct output out = { 0, samtools_stdout, NULL, &ga, KS_INITIALIZE}; //data required for output writing
+    faidx_t *fai = NULL;
+    hts_tpool *pool = NULL;
 
     static const struct option lopts[] = {
+        SAM_OPT_GLOBAL_OPTIONS('-', '-', '-', 0, '-', '@'),     //output format opt and thread count - long options
         { "output", required_argument,       NULL, 'o' },
         { "help",   no_argument,             NULL, 'h' },
         { "length", required_argument,       NULL, 'n' },
@@ -274,9 +311,20 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
         { NULL, 0, NULL, 0 }
     };
 
-    while ((c = getopt_long(argc, argv, "ho:n:cr:fi", lopts, NULL)) >= 0) {
+    while ((c = getopt_long(argc, argv, "ho:n:cr:fi@:", lopts, NULL)) >= 0) {
         switch (c) {
-            case 'o': output_file = optarg; break;
+            case 'o':
+                {
+                    output_file = optarg;
+                    char *ext = strrchr(output_file, '.');
+                    if (!ext) {
+                        break;
+                    }
+                    if (!strcmp(ext, ".gz") || !strcmp(ext, ".bgz") || !strcmp(ext, ".bgzf")) {
+                        out.isbgzip = 1;        //bgzip output
+                    }
+                    break;
+                }
             case 'n': line_len = strtol(optarg, NULL, 10);
                       if (line_len < 0) {
                         fprintf(samtools_stderr,"[faidx] bad line length '%s', using default:%d\n",optarg,ABS(DEFAULT_FASTA_LINE_LEN));
@@ -287,8 +335,14 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
             case 'r': region_file = optarg; break;
             case 'f': format = FAI_FASTQ; break;
             case 'i': rev = 1; break;
-            case '?': return usage(samtools_stderr, format, EXIT_FAILURE);
-            case 'h': return usage(samtools_stdout, format, EXIT_SUCCESS);
+            case '?':
+                exit_status = usage(samtools_stderr, format, EXIT_FAILURE);
+                goto exit2;
+                break;
+            case 'h':
+                exit_status = usage(samtools_stdout, format, EXIT_SUCCESS);
+                goto exit2;
+                break;
             case 1000:
                 if (strcmp(optarg, "no") == 0) {
                     pos_strand_name = neg_strand_name = "";
@@ -305,7 +359,7 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
                     strand_names = pos_strand_name = malloc(len + 2);
                     if (!strand_names) {
                         fprintf(samtools_stderr, "[faidx] Out of memory\n");
-                        return EXIT_FAILURE;
+                        goto exit2;
                     }
                     neg_strand_name = pos_strand_name + comma + 1;
                     memcpy(pos_strand_name, optarg + 7, comma);
@@ -316,17 +370,26 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
                     neg_strand_name[len - comma] = '\0';
                 } else {
                     fprintf(samtools_stderr, "[faidx] Unknown --mark-strand option \"%s\"\n", optarg);
-                    return usage(samtools_stderr, format, EXIT_FAILURE);
+                    exit_status = usage(samtools_stderr, format, EXIT_FAILURE);
+                    goto exit2;
                 }
                 break;
             case 1001: fai_name = optarg; break;
             case 1002: gzi_name = optarg; break;
-            default:  break;
+            // handle standard samtools options like thread count, compression level...
+            default:
+                if (parse_sam_global_opt(c, optarg, lopts, &ga)) {
+                    fprintf(samtools_stderr, "[faidx] Invalid option \"%s\"\n", optarg);
+                    goto exit2;
+                }
+                break;
         }
     }
 
-    if ( argc==optind )
-        return usage(samtools_stdout, format, EXIT_SUCCESS);
+    if ( argc==optind ) {
+        exit_status = usage(samtools_stdout, format, EXIT_SUCCESS);
+        goto exit2;
+    }
 
     if (optind+1 == argc && !region_file) {
         if (output_file && !fai_name)
@@ -343,73 +406,127 @@ int faidx_core(int argc, char *argv[], enum fai_format_options format)
             else
                 fprintf(samtools_stderr, "\n");
 
-            return EXIT_FAILURE;
+            goto exit2;
         }
+        exit_status = EXIT_SUCCESS;
+        goto exit2;
+    }
 
-        return 0;
+    if (out.gopt->nthreads > 0) {       //setup thread pool
+        if (!(pool = hts_tpool_init(out.gopt->nthreads))) {
+            fprintf(samtools_stderr, "Failed to setup thread pool\n");
+        }
     }
 
-    faidx_t *fai = fai_load3_format(argv[optind], fai_name, gzi_name, FAI_CREATE, format);
+    fai = fai_load3_format(argv[optind], fai_name, gzi_name, FAI_CREATE, format);
 
     if (!fai) {
         if (fai_name)
             fprintf(samtools_stderr, "[faidx] Could not load fai index %s", fai_name);
         else
-            fprintf(samtools_stderr, "[faidx] Could not build fai index %s.fai", argv[optind]);
+            fprintf(samtools_stderr, "[faidx] Could not load fai index %s.fai", argv[optind]);
 
         if (gzi_name)
             fprintf(samtools_stderr, " or compressed index %s\n", gzi_name);
         else
             fprintf(samtools_stderr, "\n");
 
-        return EXIT_FAILURE;
+        goto exit2;
+    }
+
+    if (pool) {                         //use thread pool if set
+        if (fai_thread_pool(fai, pool, 0)) {
+            fprintf(samtools_stderr, "Failed to set thread pool for reading\n");
+        }
     }
 
     /** output file provided by user */
     if( output_file != NULL ) {
         if( strcmp( output_file, argv[optind] ) == 0 ) {
             fprintf(samtools_stderr,"[faidx] Same input/output : %s\n", output_file);
-            return EXIT_FAILURE;
+            goto exit2;
+        }
+        if (!out.isbgzip) {
+            out.fp = fopen( output_file, "w" );
+        } else {
+            hts_opt *opts = (hts_opt *)(out.gopt->out.specific);
+            char mode[13] = "w";
+            int level = 4;                                      //default compression level
+            while (opts) {
+                if (opts->opt == HTS_OPT_COMPRESSION_LEVEL) {   //compression level
+                    level = opts->val.i;
+                    break;
+                }
+                opts = opts->next;
+            }
+            if (level >= 0) {
+                snprintf(mode, sizeof(mode), "w%d", level);     //pass compression with mode
+            }
+            out.bgzf_fp = bgzf_open(output_file, mode);
         }
 
-        file_out = fopen( output_file, "w" );
-
-        if( file_out == NULL) {
+        if( (!out.isbgzip && out.fp == NULL) || (out.isbgzip && out.bgzf_fp == NULL)) {
             fprintf(samtools_stderr,"[faidx] Cannot open \"%s\" for writing :%s.\n", output_file, strerror(errno) );
-            return EXIT_FAILURE;
+            goto exit2;
+        }
+        if (out.isbgzip && pool) {                              //use thread pool if set
+            if (bgzf_thread_pool(out.bgzf_fp, pool, 0)) {
+                fprintf(samtools_stderr, "Failed to set thread pool for writing\n");
+            }
         }
     }
 
-    int exit_status = EXIT_SUCCESS;
-
     if (region_file) {
         hFILE *rf;
 
         if ((rf = hopen(region_file, "r"))) {
-            exit_status = read_regions_from_file(fai, rf, file_out, ignore_error, line_len, rev, pos_strand_name, neg_strand_name, format);
+            exit_status = read_regions_from_file(fai, rf, &out, ignore_error, line_len, rev, pos_strand_name, neg_strand_name, format);
 
             if (hclose(rf) != 0) {
                 fprintf(samtools_stderr, "[faidx] Warning: failed to close %s", region_file);
             }
+            if (exit_status == EXIT_FAILURE) {
+                goto exit1;
+            }
         } else {
             fprintf(samtools_stderr, "[faidx] Failed to open \"%s\" for reading.\n", region_file);
-            exit_status = EXIT_FAILURE;
+            goto exit1;
         }
     }
 
+    exit_status = EXIT_SUCCESS;
     while ( ++optind<argc && exit_status == EXIT_SUCCESS) {
-        exit_status = write_output(fai, file_out, argv[optind], ignore_error, line_len, rev, pos_strand_name, neg_strand_name, format);
+        exit_status = write_output(fai, &out, argv[optind], ignore_error, line_len, rev, pos_strand_name, neg_strand_name, format);
     }
 
-    fai_destroy(fai);
-
-    if (fflush(file_out) == EOF) {
-        print_error_errno("faidx", "failed to flush output");
+    flushed = out.isbgzip ? bgzf_flush(out.bgzf_fp) : fflush(out.fp);
+    if (flushed == EOF) {
+        print_error_errno("faidx", "Failed to flush output\n");
         exit_status = EXIT_FAILURE;
     }
 
-    if( output_file != NULL) fclose(file_out);
-    free(strand_names);
+exit1:
+    if( output_file != NULL && !out.isbgzip) {
+        fclose(out.fp);     //no need to check result as already flushed
+    } else if( output_file != NULL && out.isbgzip) {
+        if (bgzf_close(out.bgzf_fp) < 0) {
+            print_error_errno("faidx", "Failed to close output\n");
+            exit_status = EXIT_FAILURE;
+        }
+    }
+
+exit2:
+    if (strand_names) {
+        free(strand_names);
+    }
+    if (fai) {
+        fai_destroy(fai);
+    }
+    if (pool) {
+        hts_tpool_destroy(pool);
+    }
+    sam_global_args_free(&ga);
+    ks_free(&out.buffer);
 
     return exit_status;
 }
index 41c0a28baa084a8c5cf424aee425769f428fdc3e..35c9358b0e7edcf71d50b8b4ec0a9d79db73b088 100644 (file)
@@ -411,7 +411,7 @@ typedef enum { full = 0, partial = 1 } earlyEnd_directive;
 int LZ4_versionNumber (void) { return LZ4_VERSION_NUMBER; }
 const char* LZ4_versionString(void) { return LZ4_VERSION_STRING; }
 int LZ4_compressBound(int isize)  { return LZ4_COMPRESSBOUND(isize); }
-int LZ4_sizeofState() { return LZ4_STREAMSIZE; }
+int LZ4_sizeofState(void) { return LZ4_STREAMSIZE; }
 
 
 /*-******************************
@@ -1434,7 +1434,7 @@ int LZ4_uncompress_unknownOutputSize (const char* source, char* dest, int isize,
 
 /* Obsolete Streaming functions */
 
-int LZ4_sizeofStreamState() { return LZ4_STREAMSIZE; }
+int LZ4_sizeofStreamState(void) { return LZ4_STREAMSIZE; }
 
 static void LZ4_init(LZ4_stream_t* lz4ds, BYTE* base)
 {
index a4a8ab43dd6c4675b7a82a732b5ca667056a6b44..74897045de341a24af465f238fbb00e5cd9643b5 100644 (file)
@@ -413,7 +413,7 @@ typedef enum { full = 0, partial = 1 } earlyEnd_directive;
 int LZ4_versionNumber (void) { return LZ4_VERSION_NUMBER; }
 const char* LZ4_versionString(void) { return LZ4_VERSION_STRING; }
 int LZ4_compressBound(int isize)  { return LZ4_COMPRESSBOUND(isize); }
-int LZ4_sizeofState() { return LZ4_STREAMSIZE; }
+int LZ4_sizeofState(void) { return LZ4_STREAMSIZE; }
 
 
 /*-******************************
@@ -1436,7 +1436,7 @@ int LZ4_uncompress_unknownOutputSize (const char* source, char* dest, int isize,
 
 /* Obsolete Streaming functions */
 
-int LZ4_sizeofStreamState() { return LZ4_STREAMSIZE; }
+int LZ4_sizeofStreamState(void) { return LZ4_STREAMSIZE; }
 
 static void LZ4_init(LZ4_stream_t* lz4ds, BYTE* base)
 {
index a4a735136befbfa74ef85c5a155b6644bed33d81..62a278f22ed52e4a1bcb5722c11e6ebe5be5a2b6 100644 (file)
@@ -116,11 +116,21 @@ static void count1(int l, const uint8_t *seq, int *cnt)
 static int **count_all(int l, int vpos, nseq_t *hash)
 {
     khint_t k;
-    int i, j, **cnt;
-    uint8_t *seq;
+    int i, j, **cnt = NULL;
+    uint8_t *seq = NULL;
+    size_t cnt_sz = ((size_t)1) << l;
+    if (cnt_sz > SSIZE_MAX / sizeof(int) / vpos) {
+        errno = ENOMEM;
+        goto fail;
+    }
     seq = calloc(l, 1);
+    if (!seq) goto fail;
     cnt = calloc(vpos, sizeof(int*));
-    for (i = 0; i < vpos; ++i) cnt[i] = calloc(1<<l, sizeof(int));
+    if (!cnt) goto fail;
+    for (i = 0; i < vpos; ++i) {
+        cnt[i] = calloc(cnt_sz, sizeof(int));
+        if (!cnt[i]) goto fail;
+    }
     for (k = 0; k < kh_end(hash); ++k) {
         if (kh_exist(hash, k)) {
             frag_t *f = &kh_val(hash, k);
@@ -138,6 +148,15 @@ static int **count_all(int l, int vpos, nseq_t *hash)
     }
     free(seq);
     return cnt;
+ fail:
+    free(seq);
+    if (cnt) {
+        for (i = 0; i < vpos; i++)
+            free(cnt[i]);
+        free(cnt);
+    }
+    print_error_errno("phase", "Couldn't allocate memory for counts");
+    return NULL;
 }
 
 // phasing
@@ -413,6 +432,7 @@ static int phase(phaseg_t *g, const char *chr, int vpos, uint64_t *cns, nseq_t *
         printf("PS\t%s\t%d\t%d\n", chr, (int)(cns[0]>>32) + 1, (int)(cns[vpos-1]>>32) + 1);
         sitemask = calloc(vpos, 1);
         cnt = count_all(g->k, vpos, hash);
+        if (!cnt) return -1;
         path = dynaprog(g->k, vpos, cnt);
         for (i = 0; i < vpos; ++i) free(cnt[i]);
         free(cnt);
index b0b525767735adb2d866835398ce5670bd3f8a6b..c239d232a56fa676c13bd3eaeb08fb5e729a37c6 100644 (file)
@@ -118,11 +118,21 @@ static void count1(int l, const uint8_t *seq, int *cnt)
 static int **count_all(int l, int vpos, nseq_t *hash)
 {
     khint_t k;
-    int i, j, **cnt;
-    uint8_t *seq;
+    int i, j, **cnt = NULL;
+    uint8_t *seq = NULL;
+    size_t cnt_sz = ((size_t)1) << l;
+    if (cnt_sz > SSIZE_MAX / sizeof(int) / vpos) {
+        errno = ENOMEM;
+        goto fail;
+    }
     seq = calloc(l, 1);
+    if (!seq) goto fail;
     cnt = calloc(vpos, sizeof(int*));
-    for (i = 0; i < vpos; ++i) cnt[i] = calloc(1<<l, sizeof(int));
+    if (!cnt) goto fail;
+    for (i = 0; i < vpos; ++i) {
+        cnt[i] = calloc(cnt_sz, sizeof(int));
+        if (!cnt[i]) goto fail;
+    }
     for (k = 0; k < kh_end(hash); ++k) {
         if (kh_exist(hash, k)) {
             frag_t *f = &kh_val(hash, k);
@@ -140,6 +150,15 @@ static int **count_all(int l, int vpos, nseq_t *hash)
     }
     free(seq);
     return cnt;
+ fail:
+    free(seq);
+    if (cnt) {
+        for (i = 0; i < vpos; i++)
+            free(cnt[i]);
+        free(cnt);
+    }
+    print_error_errno("phase", "Couldn't allocate memory for counts");
+    return NULL;
 }
 
 // phasing
@@ -415,6 +434,7 @@ static int phase(phaseg_t *g, const char *chr, int vpos, uint64_t *cns, nseq_t *
         fprintf(samtools_stdout, "PS\t%s\t%d\t%d\n", chr, (int)(cns[0]>>32) + 1, (int)(cns[vpos-1]>>32) + 1);
         sitemask = calloc(vpos, 1);
         cnt = count_all(g->k, vpos, hash);
+        if (!cnt) return -1;
         path = dynaprog(g->k, vpos, cnt);
         for (i = 0; i < vpos; ++i) free(cnt[i]);
         free(cnt);
index 4e522cddf13a59b4575476a632aa23b28555f442..8f76a564ea20b0642bc1bdbb88b3c2d45e992353 100644 (file)
@@ -1,7 +1,7 @@
 /*  reset.c --  removes aligner updates and reference data from input sam /
                 bam / cram file and makes read data raw for new processing
 
-    Copyright (C) 2022, 2023 Genome Research Ltd.
+    Copyright (C) 2022 - 2024 Genome Research Ltd.
 
     Author: Vasudeva Sarma <vasudeva.sarma@sanger.ac.uk>
 
@@ -38,10 +38,13 @@ DEALINGS IN THE SOFTWARE
 #define TAGNUM(X) (((X)[0] << 8) | (X)[1])  //to create key for aux tags, like type key in htslib
 #define LONG_OPT(X) (128 + (X))             //to handle long and short options with same char
 
+#define RESET_KEEPDUPFLAG   1               //keep dup flag as such, as in initial implementation
+
 typedef struct conf_data
 {
     int keepRGs;                    //RG line handling
     int noPGentry;                  //PG line for reset op or not
+    int ctrlFlags;                  //control flags
     auxhash_t aux_keep;             //SET that holds the aux tags to be retained
     auxhash_t aux_remove;           //SET that holds the aux tags to be removed
     char *pgid;                     //PG id onwards which to be removed
@@ -62,49 +65,74 @@ static void usage(FILE *fp)
       --reject-PG ID\n\
                Removes PG line with ID matching to input and succeeding PG lines\n\
       --no-RG  To have RG lines or not\n\
-      --no-PG  To have PG entry or not for reset operation\n");
+      --no-PG  To have PG entry or not for reset operation\n\
+      --dupflag\n\
+               Keeps the duplicate flag as it is\n");
 
     sam_global_opt_help(fp, "--O--@--");
     return;
 }
 
-/// removeauxtags - remove aux tags in bam data which are not present in acceptable tag set
-/** @param bamdata - pointer to the bamdata from which needs the filtering
- *  @param config - pointer to conf_data
+/// update_aux_conf - update the user given aux tag configuration with defaults
+/** @param config - pointer to conf_data
 returns nothing
 */
-void removeauxtags(bam1_t *bamdata, conf_data *config)
+void update_aux_conf(conf_data *config)
 {
-    uint8_t *auxdata = NULL;
-    const char *tag = NULL, rg[] = "RG";
+    const char rg[] = "RG";
+    const char *default_tags[] = {"AS", "CC", "CG", "CP", "H1", "H2", "HI", "H0", "IH",
+                                    "MC", "MD", "MQ", "NM", "SA", "TS"};
     khint_t iter = 0;
-    int ret = 0;
+    int ret = 0, i = 0;
 
-    if (!bamdata || !config || (!config->aux_keep && !config->aux_remove && config->keepRGs))
+    if (!config)
         return;
 
-    //remove RG tags from bamdata if keepRG is false
-    if (!config->keepRGs) {
-        if (!config->aux_keep && !config->aux_remove) {
-            //none of aux tag filter in use, create remove filter
-            config->aux_remove = kh_init(aux_exists);
-        }
-
-        if (config->aux_keep) {
-            //keep set in use, remove RG if present
+    if (!config->aux_keep && !config->aux_remove) {
+        //none of aux tag filter in use, create remove filter
+        config->aux_remove = kh_init(aux_exists);
+    }
+    if (config->aux_keep) {
+        //keep set in use, remove RG if present
+        if (!config->keepRGs) {
             iter = kh_get(aux_exists, config->aux_keep, TAGNUM(rg));
             if (iter != kh_end(config->aux_keep)) {
                 kh_del(aux_exists, config->aux_keep, iter);
             }
         }
-        if (config->aux_remove) {
+    }
+    if (config->aux_remove) {
+        if (!config->keepRGs) {
             //remove set in use, add RG if not present
             iter = kh_get(aux_exists, config->aux_remove, TAGNUM(rg));
             if (iter == kh_end(config->aux_remove)) {
                 kh_put(aux_exists, config->aux_remove, TAGNUM(rg), &ret);
             }
         }
+        //add the default tags if not present in remove set
+        //note, keep has priority and this may not be honoured
+        for (i = 0; i < sizeof(default_tags) / sizeof(default_tags[0]); ++i) {
+            iter = kh_get(aux_exists, config->aux_remove, TAGNUM(default_tags[i]));
+            if (iter == kh_end(config->aux_remove)) {
+                kh_put(aux_exists, config->aux_remove, TAGNUM(default_tags[i]), &ret);
+            }
+        }
     }
+}
+
+/// removeauxtags - remove aux tags in bam data which are not present in acceptable tag set
+/** @param bamdata - pointer to the bamdata from which needs the filtering
+ *  @param config - pointer to conf_data
+returns nothing
+*/
+void removeauxtags(bam1_t *bamdata, conf_data *config)
+{
+    uint8_t *auxdata = NULL;
+    const char *tag = NULL;
+    khint_t iter = 0;
+
+    if (!bamdata || !config)
+        return;
 
     for (auxdata = bam_aux_first(bamdata); auxdata; ) {
         tag = bam_aux_tag(auxdata);
@@ -322,7 +350,6 @@ int reset(samFile *infile, samFile *outfile, conf_data *config, char *args)
         if (bamdata->core.flag & BAM_FSECONDARY || bamdata->core.flag & BAM_FSUPPLEMENTARY) {
             continue;
         }
-
         //update flags
         uint16_t flags = bamdata->core.flag & ~BAM_FPROPER_PAIR;    //reset pair info
         flags |= BAM_FUNMAP;                                        //mark as unmapped
@@ -330,6 +357,9 @@ int reset(samFile *infile, samFile *outfile, conf_data *config, char *args)
             flags |= BAM_FMUNMAP;                                   //mark mate as unmapped, if it was a pair
         }
         flags &= ~BAM_FMREVERSE;                                    //reset mate orientation
+        if (!(config->ctrlFlags & RESET_KEEPDUPFLAG)) {
+            flags &= ~BAM_FDUP;                                     //reset dup flag from alignment
+        }
 
         if (0 > ks_resize(&querydata, bamdata->core.l_qseq) ||
             0 > ks_resize(&qualdata, bamdata->core.l_qseq)) {
@@ -439,6 +469,7 @@ int main_reset(int argc, char *argv[])
         //reject PG lines from input, default is to keep them (i.e. option not given); without optional filename, all PGs removed and those given in file are filtered when optional filename is given
         {"reject-PG", required_argument, NULL, 'p'},                //reject entries from this PG onwards
         {"no-PG", no_argument, NULL, 2},                            //do not add PG entry for reset operation, default is to add it
+        {"dupflag", no_argument, NULL, 3},                          //keep the dup flag as it is - as in initial reset implementation
         {NULL, 0, NULL, 0}
     };
     samFile *infile = NULL, *outfile = NULL;
@@ -447,8 +478,7 @@ int main_reset(int argc, char *argv[])
     const char *inname = NULL, *outname = NULL;
     int c = 0, ret = EXIT_FAILURE;
     char outmode[4] = "w", *args = NULL;
-    conf_data resetconf = {1, 0, NULL, NULL, NULL};                //keep RGs and PGs by default
-
+    conf_data resetconf = {1, 0, 0, NULL, NULL, NULL};              //keep RGs and PGs by default, ctrlflags = 0
 
     //samtools reset -o outfile -x/--remove-tag ... --keep-tag ... --threads=n --output-fmt=fmt --no-RG --reject-PG pgid --no-PG [<infile>]
     while ((c = getopt_long(argc, argv, "o:@:x:O:", lopts, NULL)) >= 0)
@@ -469,6 +499,9 @@ int main_reset(int argc, char *argv[])
             }
             resetconf.noPGentry = 1;
             break;
+        case 3:                             //keep dup flag as it is / no reset
+            resetconf.ctrlFlags |= RESET_KEEPDUPFLAG;
+            break;
         case 'p':                           //--reject-PG=<id>
             if (resetconf.pgid) {
                 usage(stderr);              //already given!
@@ -498,7 +531,7 @@ int main_reset(int argc, char *argv[])
                 }
             }
             break;
-        case LONG_OPT('x'):                  //keep aux tags
+        case LONG_OPT('x'):                 //keep aux tags
             if (parse_aux_list(&resetconf.aux_keep, optarg, "main_reset")) {
                 usage(stderr);
                 goto exit;
@@ -542,6 +575,8 @@ int main_reset(int argc, char *argv[])
         inname = "-";
     }
 
+    //update aux tag configuration
+    update_aux_conf(&resetconf);
     //set output file format based on name
     sam_open_mode(outmode + 1, outname, NULL);
 
index c98946f0228ff81bac07e6fe149efc95c83bf12a..db1dba7dad89fbe1b2df62e697bcf1c25a00f3e2 100644 (file)
@@ -3,7 +3,7 @@
 /*  reset.c --  removes aligner updates and reference data from input sam /
                 bam / cram file and makes read data raw for new processing
 
-    Copyright (C) 2022, 2023 Genome Research Ltd.
+    Copyright (C) 2022 - 2024 Genome Research Ltd.
 
     Author: Vasudeva Sarma <vasudeva.sarma@sanger.ac.uk>
 
@@ -40,10 +40,13 @@ DEALINGS IN THE SOFTWARE
 #define TAGNUM(X) (((X)[0] << 8) | (X)[1])  //to create key for aux tags, like type key in htslib
 #define LONG_OPT(X) (128 + (X))             //to handle long and short options with same char
 
+#define RESET_KEEPDUPFLAG   1               //keep dup flag as such, as in initial implementation
+
 typedef struct conf_data
 {
     int keepRGs;                    //RG line handling
     int noPGentry;                  //PG line for reset op or not
+    int ctrlFlags;                  //control flags
     auxhash_t aux_keep;             //SET that holds the aux tags to be retained
     auxhash_t aux_remove;           //SET that holds the aux tags to be removed
     char *pgid;                     //PG id onwards which to be removed
@@ -64,49 +67,74 @@ static void usage(FILE *fp)
       --reject-PG ID\n\
                Removes PG line with ID matching to input and succeeding PG lines\n\
       --no-RG  To have RG lines or not\n\
-      --no-PG  To have PG entry or not for reset operation\n");
+      --no-PG  To have PG entry or not for reset operation\n\
+      --dupflag\n\
+               Keeps the duplicate flag as it is\n");
 
     sam_global_opt_help(fp, "--O--@--");
     return;
 }
 
-/// removeauxtags - remove aux tags in bam data which are not present in acceptable tag set
-/** @param bamdata - pointer to the bamdata from which needs the filtering
- *  @param config - pointer to conf_data
+/// update_aux_conf - update the user given aux tag configuration with defaults
+/** @param config - pointer to conf_data
 returns nothing
 */
-void removeauxtags(bam1_t *bamdata, conf_data *config)
+void update_aux_conf(conf_data *config)
 {
-    uint8_t *auxdata = NULL;
-    const char *tag = NULL, rg[] = "RG";
+    const char rg[] = "RG";
+    const char *default_tags[] = {"AS", "CC", "CG", "CP", "H1", "H2", "HI", "H0", "IH",
+                                    "MC", "MD", "MQ", "NM", "SA", "TS"};
     khint_t iter = 0;
-    int ret = 0;
+    int ret = 0, i = 0;
 
-    if (!bamdata || !config || (!config->aux_keep && !config->aux_remove && config->keepRGs))
+    if (!config)
         return;
 
-    //remove RG tags from bamdata if keepRG is false
-    if (!config->keepRGs) {
-        if (!config->aux_keep && !config->aux_remove) {
-            //none of aux tag filter in use, create remove filter
-            config->aux_remove = kh_init(aux_exists);
-        }
-
-        if (config->aux_keep) {
-            //keep set in use, remove RG if present
+    if (!config->aux_keep && !config->aux_remove) {
+        //none of aux tag filter in use, create remove filter
+        config->aux_remove = kh_init(aux_exists);
+    }
+    if (config->aux_keep) {
+        //keep set in use, remove RG if present
+        if (!config->keepRGs) {
             iter = kh_get(aux_exists, config->aux_keep, TAGNUM(rg));
             if (iter != kh_end(config->aux_keep)) {
                 kh_del(aux_exists, config->aux_keep, iter);
             }
         }
-        if (config->aux_remove) {
+    }
+    if (config->aux_remove) {
+        if (!config->keepRGs) {
             //remove set in use, add RG if not present
             iter = kh_get(aux_exists, config->aux_remove, TAGNUM(rg));
             if (iter == kh_end(config->aux_remove)) {
                 kh_put(aux_exists, config->aux_remove, TAGNUM(rg), &ret);
             }
         }
+        //add the default tags if not present in remove set
+        //note, keep has priority and this may not be honoured
+        for (i = 0; i < sizeof(default_tags) / sizeof(default_tags[0]); ++i) {
+            iter = kh_get(aux_exists, config->aux_remove, TAGNUM(default_tags[i]));
+            if (iter == kh_end(config->aux_remove)) {
+                kh_put(aux_exists, config->aux_remove, TAGNUM(default_tags[i]), &ret);
+            }
+        }
     }
+}
+
+/// removeauxtags - remove aux tags in bam data which are not present in acceptable tag set
+/** @param bamdata - pointer to the bamdata from which needs the filtering
+ *  @param config - pointer to conf_data
+returns nothing
+*/
+void removeauxtags(bam1_t *bamdata, conf_data *config)
+{
+    uint8_t *auxdata = NULL;
+    const char *tag = NULL;
+    khint_t iter = 0;
+
+    if (!bamdata || !config)
+        return;
 
     for (auxdata = bam_aux_first(bamdata); auxdata; ) {
         tag = bam_aux_tag(auxdata);
@@ -324,7 +352,6 @@ int reset(samFile *infile, samFile *outfile, conf_data *config, char *args)
         if (bamdata->core.flag & BAM_FSECONDARY || bamdata->core.flag & BAM_FSUPPLEMENTARY) {
             continue;
         }
-
         //update flags
         uint16_t flags = bamdata->core.flag & ~BAM_FPROPER_PAIR;    //reset pair info
         flags |= BAM_FUNMAP;                                        //mark as unmapped
@@ -332,6 +359,9 @@ int reset(samFile *infile, samFile *outfile, conf_data *config, char *args)
             flags |= BAM_FMUNMAP;                                   //mark mate as unmapped, if it was a pair
         }
         flags &= ~BAM_FMREVERSE;                                    //reset mate orientation
+        if (!(config->ctrlFlags & RESET_KEEPDUPFLAG)) {
+            flags &= ~BAM_FDUP;                                     //reset dup flag from alignment
+        }
 
         if (0 > ks_resize(&querydata, bamdata->core.l_qseq) ||
             0 > ks_resize(&qualdata, bamdata->core.l_qseq)) {
@@ -441,6 +471,7 @@ int main_reset(int argc, char *argv[])
         //reject PG lines from input, default is to keep them (i.e. option not given); without optional filename, all PGs removed and those given in file are filtered when optional filename is given
         {"reject-PG", required_argument, NULL, 'p'},                //reject entries from this PG onwards
         {"no-PG", no_argument, NULL, 2},                            //do not add PG entry for reset operation, default is to add it
+        {"dupflag", no_argument, NULL, 3},                          //keep the dup flag as it is - as in initial reset implementation
         {NULL, 0, NULL, 0}
     };
     samFile *infile = NULL, *outfile = NULL;
@@ -449,8 +480,7 @@ int main_reset(int argc, char *argv[])
     const char *inname = NULL, *outname = NULL;
     int c = 0, ret = EXIT_FAILURE;
     char outmode[4] = "w", *args = NULL;
-    conf_data resetconf = {1, 0, NULL, NULL, NULL};                //keep RGs and PGs by default
-
+    conf_data resetconf = {1, 0, 0, NULL, NULL, NULL};              //keep RGs and PGs by default, ctrlflags = 0
 
     //samtools reset -o outfile -x/--remove-tag ... --keep-tag ... --threads=n --output-fmt=fmt --no-RG --reject-PG pgid --no-PG [<infile>]
     while ((c = getopt_long(argc, argv, "o:@:x:O:", lopts, NULL)) >= 0)
@@ -471,6 +501,9 @@ int main_reset(int argc, char *argv[])
             }
             resetconf.noPGentry = 1;
             break;
+        case 3:                             //keep dup flag as it is / no reset
+            resetconf.ctrlFlags |= RESET_KEEPDUPFLAG;
+            break;
         case 'p':                           //--reject-PG=<id>
             if (resetconf.pgid) {
                 usage(samtools_stderr);              //already given!
@@ -500,7 +533,7 @@ int main_reset(int argc, char *argv[])
                 }
             }
             break;
-        case LONG_OPT('x'):                  //keep aux tags
+        case LONG_OPT('x'):                 //keep aux tags
             if (parse_aux_list(&resetconf.aux_keep, optarg, "main_reset")) {
                 usage(samtools_stderr);
                 goto exit;
@@ -544,6 +577,8 @@ int main_reset(int argc, char *argv[])
         inname = "-";
     }
 
+    //update aux tag configuration
+    update_aux_conf(&resetconf);
     //set output file format based on name
     sam_open_mode(outmode + 1, outname, NULL);
 
index d7178b2e524f8479f999cc2e77dd4ad62eb3d271..d12a1e65606a918b91b6bdf4fe064395e87e1c64 100644 (file)
@@ -42,7 +42,8 @@ void release_autoflush(htsFile *fp) {
     if (samtools_stdout == fp) samtools_stdout = NULL;
 }
 
-static void vprint_error_core(const char *subcommand, const char *format, va_list args, const char *extra)
+static void HTS_FORMAT(HTS_PRINTF_FMT, 2, 0)
+vprint_error_core(const char *subcommand, const char *format, va_list args, const char *extra)
 {
     fflush(stdout);
     if (samtools_stdout) hts_flush(samtools_stdout);
index 304dd382742c85cf474e677ee6ff981824d8116f..a32921993b373538c4a658ec2891cd716fba2885 100644 (file)
@@ -44,7 +44,8 @@ void release_autoflush(htsFile *fp) {
     if (samtools_stdout_internal == fp) samtools_stdout_internal = NULL;
 }
 
-static void vprint_error_core(const char *subcommand, const char *format, va_list args, const char *extra)
+static void HTS_FORMAT(HTS_PRINTF_FMT, 2, 0)
+vprint_error_core(const char *subcommand, const char *format, va_list args, const char *extra)
 {
     fflush(samtools_stdout);
     if (samtools_stdout_internal) hts_flush(samtools_stdout_internal);
index aa5b92310dce313f56cee4409062bada951562f2..6afd653f28c715cdee062b3521e19396bb0c43b4 100644 (file)
@@ -1,6 +1,6 @@
 /*  sam_view.c -- SAM<->BAM<->CRAM conversion.
 
-    Copyright (C) 2009-2023 Genome Research Ltd.
+    Copyright (C) 2009-2024 Genome Research Ltd.
     Portions copyright (C) 2009, 2011, 2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -54,6 +54,8 @@ typedef struct samview_settings {
     strhash_t rnhash;
     strhash_t tvhash;
     int min_mapQ;
+    int rghash_discard; // 0 keep, 1 discard
+    int rnhash_discard; // 0 keep, 1 discard
 
     // Described here in the same terms as the usage statement.
     // The code however always negates to "reject if"         keep if:
@@ -176,7 +178,8 @@ static int process_aln(const sam_hdr_t *h, bam1_t *b, samview_settings_t* settin
         uint8_t *s = bam_aux_get(b, "RG");
         if (s) {
             khint_t k = kh_get(str, settings->rghash, (char*)(s + 1));
-            if (k == kh_end(settings->rghash)) return 1;
+            if ((k == kh_end(settings->rghash)) != settings->rghash_discard)
+                return 1;
         }
     }
     if (settings->tag) {
@@ -204,9 +207,11 @@ static int process_aln(const sam_hdr_t *h, bam1_t *b, samview_settings_t* settin
     }
     if (settings->rnhash) {
         const char* rn = bam_get_qname(b);
-        if (!rn || kh_get(str, settings->rnhash, rn) == kh_end(settings->rnhash)) {
+        strhash_t h = settings->rnhash;
+        if (!rn && !settings->rnhash_discard)
+            return 1;
+        if ((kh_get(str, h, rn) == kh_end(h)) != settings->rnhash_discard)
             return 1;
-        }
     }
     if (settings->library) {
         const char *p = bam_get_library((sam_hdr_t*)h, b);
@@ -305,7 +310,12 @@ static int add_read_group_single(const char *subcmd, samview_settings_t *setting
     if (settings->rghash == NULL) {
         settings->rghash = kh_init(str);
         if (settings->rghash == NULL) goto err;
+    } else if (settings->rghash_discard == 1) {
+        print_error("view", "cannot mix include and exclude read-group files in the same command line");
+        free(d);
+        return -1;
     }
+    settings->rghash_discard = 0;
 
     kh_put(str, settings->rghash, d, &ret);
     if (ret == -1) goto err;
@@ -326,8 +336,14 @@ static int add_read_names_file(const char *subcmd, samview_settings_t *settings,
             perror(NULL);
             return -1;
         }
+    } else if ((settings->rnhash_discard == 0 && *fn == '^') ||
+        (settings->rnhash_discard == 1 && *fn != '^')) {
+        print_error("view", "cannot mix include and exclude read-name files in the same command line");
+        return -1;
     }
-    return populate_lookup_from_file(subcmd, settings->rnhash, fn);
+    settings->rnhash_discard = (*fn == '^');
+    return populate_lookup_from_file(subcmd, settings->rnhash,
+                                     fn + (*fn == '^'));
 }
 
 static int add_read_groups_file(const char *subcmd, samview_settings_t *settings, char *fn)
@@ -338,8 +354,14 @@ static int add_read_groups_file(const char *subcmd, samview_settings_t *settings
             perror(NULL);
             return -1;
         }
+    } else if ((settings->rghash_discard == 0 && *fn == '^') ||
+        (settings->rghash_discard == 1 && *fn != '^')) {
+        print_error("view", "cannot mix include and exclude read-group files in the same command line");
+        return -1;
     }
-    return populate_lookup_from_file(subcmd, settings->rghash, fn);
+    settings->rghash_discard = (*fn == '^');
+    return populate_lookup_from_file(subcmd, settings->rghash,
+                                     fn + (*fn == '^'));
 }
 
 static int add_tag_value_single(const char *subcmd, samview_settings_t *settings, char *name)
@@ -533,12 +555,20 @@ hts_itr_multi_t *multi_region_init(samview_settings_t *conf, char **regs, int nr
     int filter_state = ALL;
     if ( nregs ) {
         int filter_op = 0;
-        conf->bed = bed_hash_regions(conf->bed, regs, 0, nregs, &filter_op); // insert(1) or filter out(0) the regions from the command line in the same hash table as the bed file
+        void *bed = bed_hash_regions(conf->bed, regs, 0, nregs, &filter_op); // insert(1) or filter out(0) the regions from the command line in the same hash table as the bed file
+        if (!bed) {
+            print_error_errno("view", "Couldn't %s region list",
+                              filter_op ? "build" : "filter");
+            return NULL;
+        }
+        conf->bed = bed;
         if ( !filter_op )
             filter_state = FILTERED;
     }
     else
         bed_unify(conf->bed);
+
+    // This check is probably redundant, but left just in case
     if ( !conf->bed) { // index is unavailable or no regions have been specified
         print_error("view", "No regions or BED file have been provided. Aborting.");
         return NULL;
@@ -609,8 +639,9 @@ static int fetch_pairs_collect_mates(samview_settings_t *conf, hts_itr_multi_t *
             }
         }
 
-        if ( rec->core.mtid < 0 || (rec->core.flag & BAM_FMUNMAP) ) nunmap = 1;
-        if ( rec->core.mtid >= 0 ) {
+        if ( rec->core.mtid < 0 ) {
+            nunmap = 1;
+        } else {
             if (_reglist_push(&conf->reglist, &conf->nreglist, rec->core.mtid, rec->core.mpos,rec->core.mpos+1) != 0)
                 goto out;
         }
@@ -755,13 +786,13 @@ static int multi_region_view(samview_settings_t *conf, hts_itr_multi_t *iter)
     while ((result = sam_itr_multi_next(conf->in, iter, b)) >= 0) {
         if (process_one_record(conf, b, &write_error) < 0) break;
     }
-    hts_itr_multi_destroy(iter);
     bam_destroy1(b);
 
     if (result < -1) {
         print_error("view", "retrieval of region #%d failed", iter->curr_tid);
-        return 1;
+        write_error = 1;
     }
+    hts_itr_multi_destroy(iter);
     return write_error;
 }
 
@@ -853,6 +884,7 @@ int main_samview(int argc, char *argv[])
         {"use-index", no_argument, NULL, 'M'},
         {"with-header", no_argument, NULL, 'h'},
         {"sanitize", required_argument, NULL, 'z'},
+        {NULL, 0, NULL, 0}
     };
 
     /* parse command-line options */
@@ -868,6 +900,8 @@ int main_samview(int argc, char *argv[])
     opterr = 0;
 
     char *tmp;
+    int tmp_flag;
+
     while ((c = getopt_long(argc, argv,
                             "SbBcCt:h1Ho:O:q:f:F:G:ul:r:T:R:N:d:D:L:s:@:m:x:U:MXe:pPz:",
                             lopts, NULL)) >= 0) {
@@ -912,19 +946,47 @@ int main_samview(int argc, char *argv[])
         case 'U': settings.fn_un_out = strdup(optarg); break;
         case 'X': has_index_file = 1; break;
         case 'f':
-            settings.flag_on |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.flag_on |= tmp_flag;
             settings.count_rf |= SAM_FLAG | SAM_RNEXT;
             break;
         case 'F':
-            settings.flag_off |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.flag_off |= tmp_flag;
             settings.count_rf |= SAM_FLAG | SAM_RNEXT;
             break;
         case LONGOPT('g'):
-            settings.flag_anyon |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.flag_anyon |= tmp_flag;
             settings.count_rf |= SAM_FLAG | SAM_RNEXT;
             break;
         case 'G':
-            settings.flag_alloff |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.flag_alloff |= tmp_flag;
             settings.count_rf |= SAM_FLAG | SAM_RNEXT;
             break;
         case 'q':
@@ -1084,8 +1146,27 @@ int main_samview(int argc, char *argv[])
             }
             settings.count_rf = INT_MAX; // no way to know what we need
             break;
-        case LONGOPT('r'): settings.remove_flag |= bam_str2flag(optarg); break;
-        case LONGOPT('a'): settings.add_flag |= bam_str2flag(optarg); break;
+        case LONGOPT('r'):
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.remove_flag |= tmp_flag;
+            break;
+
+        case LONGOPT('a'):
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.add_flag |= tmp_flag;
+            break;
 
         case 'x':
             if (*optarg == '^') {
@@ -1354,8 +1435,6 @@ int main_samview(int argc, char *argv[])
         }
     }
 
-    if ( settings.hts_idx ) hts_idx_destroy(settings.hts_idx);
-
     if (ga.write_index) {
         if (sam_idx_save(settings.out) < 0) {
             print_error_errno("view", "writing index failed");
@@ -1368,6 +1447,8 @@ int main_samview(int argc, char *argv[])
     }
 
 view_end:
+    if ( settings.hts_idx ) hts_idx_destroy(settings.hts_idx);
+
     if (settings.is_count && ret == 0) {
         if (fprintf(settings.fn_out? fp_out : stdout, "%" PRId64 "\n", settings.count) < 0) {
             if (settings.fn_out) print_error_errno("view", "writing to \"%s\" failed", settings.fn_out);
@@ -1458,9 +1539,10 @@ static int usage(FILE *fp, int exit_status, int is_long_help)
 "\n"
 "Filtering options (Only include in output reads that...):\n"
 "  -L, --target[s]-file FILE  ...overlap (BED) regions in FILE\n"
+"  -N, --qname-file [^]FILE   ...whose read name is listed in FILE (\"^\" negates)\n"
 "  -r, --read-group STR       ...are in read group STR\n"
-"  -R, --read-group-file FILE ...are in a read group listed in FILE\n"
-"  -N, --qname-file FILE      ...whose read name is listed in FILE\n"
+"  -R, --read-group-file [^]FILE\n"
+"                             ...are in a read group listed in FILE\n"
 "  -d, --tag STR1[:STR2]      ...have a tag STR1 (with associated value STR2)\n"
 "  -D, --tag-file STR:FILE    ...have a tag STR whose value is listed in FILE\n"
 "  -q, --min-MQ INT           ...have mapping quality >= INT\n"
@@ -1634,7 +1716,7 @@ int main_head(int argc, char *argv[])
     if (nrecords > 0) {
         b = bam_init1();
         uint64_t n;
-        int r;
+        int r = 0;
         for (n = 0; n < nrecords && (r = sam_read1(fp, hdr, b)) >= 0; n++) {
             if (sam_format1(hdr, b, &str) < 0) {
                 print_error_errno("head", "couldn't format record");
index e1b681b820ac50bc105d39bdaf63609c4110f28f..d1f55ee9f0177722b4cdef340d7e0ca116b11912 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  sam_view.c -- SAM<->BAM<->CRAM conversion.
 
-    Copyright (C) 2009-2023 Genome Research Ltd.
+    Copyright (C) 2009-2024 Genome Research Ltd.
     Portions copyright (C) 2009, 2011, 2012 Broad Institute.
 
     Author: Heng Li <lh3@sanger.ac.uk>
@@ -56,6 +56,8 @@ typedef struct samview_settings {
     strhash_t rnhash;
     strhash_t tvhash;
     int min_mapQ;
+    int rghash_discard; // 0 keep, 1 discard
+    int rnhash_discard; // 0 keep, 1 discard
 
     // Described here in the same terms as the usage statement.
     // The code however always negates to "reject if"         keep if:
@@ -178,7 +180,8 @@ static int process_aln(const sam_hdr_t *h, bam1_t *b, samview_settings_t* settin
         uint8_t *s = bam_aux_get(b, "RG");
         if (s) {
             khint_t k = kh_get(str, settings->rghash, (char*)(s + 1));
-            if (k == kh_end(settings->rghash)) return 1;
+            if ((k == kh_end(settings->rghash)) != settings->rghash_discard)
+                return 1;
         }
     }
     if (settings->tag) {
@@ -206,9 +209,11 @@ static int process_aln(const sam_hdr_t *h, bam1_t *b, samview_settings_t* settin
     }
     if (settings->rnhash) {
         const char* rn = bam_get_qname(b);
-        if (!rn || kh_get(str, settings->rnhash, rn) == kh_end(settings->rnhash)) {
+        strhash_t h = settings->rnhash;
+        if (!rn && !settings->rnhash_discard)
+            return 1;
+        if ((kh_get(str, h, rn) == kh_end(h)) != settings->rnhash_discard)
             return 1;
-        }
     }
     if (settings->library) {
         const char *p = bam_get_library((sam_hdr_t*)h, b);
@@ -307,7 +312,12 @@ static int add_read_group_single(const char *subcmd, samview_settings_t *setting
     if (settings->rghash == NULL) {
         settings->rghash = kh_init(str);
         if (settings->rghash == NULL) goto err;
+    } else if (settings->rghash_discard == 1) {
+        print_error("view", "cannot mix include and exclude read-group files in the same command line");
+        free(d);
+        return -1;
     }
+    settings->rghash_discard = 0;
 
     kh_put(str, settings->rghash, d, &ret);
     if (ret == -1) goto err;
@@ -328,8 +338,14 @@ static int add_read_names_file(const char *subcmd, samview_settings_t *settings,
             perror(NULL);
             return -1;
         }
+    } else if ((settings->rnhash_discard == 0 && *fn == '^') ||
+        (settings->rnhash_discard == 1 && *fn != '^')) {
+        print_error("view", "cannot mix include and exclude read-name files in the same command line");
+        return -1;
     }
-    return populate_lookup_from_file(subcmd, settings->rnhash, fn);
+    settings->rnhash_discard = (*fn == '^');
+    return populate_lookup_from_file(subcmd, settings->rnhash,
+                                     fn + (*fn == '^'));
 }
 
 static int add_read_groups_file(const char *subcmd, samview_settings_t *settings, char *fn)
@@ -340,8 +356,14 @@ static int add_read_groups_file(const char *subcmd, samview_settings_t *settings
             perror(NULL);
             return -1;
         }
+    } else if ((settings->rghash_discard == 0 && *fn == '^') ||
+        (settings->rghash_discard == 1 && *fn != '^')) {
+        print_error("view", "cannot mix include and exclude read-group files in the same command line");
+        return -1;
     }
-    return populate_lookup_from_file(subcmd, settings->rghash, fn);
+    settings->rghash_discard = (*fn == '^');
+    return populate_lookup_from_file(subcmd, settings->rghash,
+                                     fn + (*fn == '^'));
 }
 
 static int add_tag_value_single(const char *subcmd, samview_settings_t *settings, char *name)
@@ -535,12 +557,20 @@ hts_itr_multi_t *multi_region_init(samview_settings_t *conf, char **regs, int nr
     int filter_state = ALL;
     if ( nregs ) {
         int filter_op = 0;
-        conf->bed = bed_hash_regions(conf->bed, regs, 0, nregs, &filter_op); // insert(1) or filter out(0) the regions from the command line in the same hash table as the bed file
+        void *bed = bed_hash_regions(conf->bed, regs, 0, nregs, &filter_op); // insert(1) or filter out(0) the regions from the command line in the same hash table as the bed file
+        if (!bed) {
+            print_error_errno("view", "Couldn't %s region list",
+                              filter_op ? "build" : "filter");
+            return NULL;
+        }
+        conf->bed = bed;
         if ( !filter_op )
             filter_state = FILTERED;
     }
     else
         bed_unify(conf->bed);
+
+    // This check is probably redundant, but left just in case
     if ( !conf->bed) { // index is unavailable or no regions have been specified
         print_error("view", "No regions or BED file have been provided. Aborting.");
         return NULL;
@@ -611,8 +641,9 @@ static int fetch_pairs_collect_mates(samview_settings_t *conf, hts_itr_multi_t *
             }
         }
 
-        if ( rec->core.mtid < 0 || (rec->core.flag & BAM_FMUNMAP) ) nunmap = 1;
-        if ( rec->core.mtid >= 0 ) {
+        if ( rec->core.mtid < 0 ) {
+            nunmap = 1;
+        } else {
             if (_reglist_push(&conf->reglist, &conf->nreglist, rec->core.mtid, rec->core.mpos,rec->core.mpos+1) != 0)
                 goto out;
         }
@@ -757,13 +788,13 @@ static int multi_region_view(samview_settings_t *conf, hts_itr_multi_t *iter)
     while ((result = sam_itr_multi_next(conf->in, iter, b)) >= 0) {
         if (process_one_record(conf, b, &write_error) < 0) break;
     }
-    hts_itr_multi_destroy(iter);
     bam_destroy1(b);
 
     if (result < -1) {
         print_error("view", "retrieval of region #%d failed", iter->curr_tid);
-        return 1;
+        write_error = 1;
     }
+    hts_itr_multi_destroy(iter);
     return write_error;
 }
 
@@ -855,6 +886,7 @@ int main_samview(int argc, char *argv[])
         {"use-index", no_argument, NULL, 'M'},
         {"with-header", no_argument, NULL, 'h'},
         {"sanitize", required_argument, NULL, 'z'},
+        {NULL, 0, NULL, 0}
     };
 
     /* parse command-line options */
@@ -870,6 +902,8 @@ int main_samview(int argc, char *argv[])
     opterr = 0;
 
     char *tmp;
+    int tmp_flag;
+
     while ((c = getopt_long(argc, argv,
                             "SbBcCt:h1Ho:O:q:f:F:G:ul:r:T:R:N:d:D:L:s:@:m:x:U:MXe:pPz:",
                             lopts, NULL)) >= 0) {
@@ -914,19 +948,47 @@ int main_samview(int argc, char *argv[])
         case 'U': settings.fn_un_out = strdup(optarg); break;
         case 'X': has_index_file = 1; break;
         case 'f':
-            settings.flag_on |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.flag_on |= tmp_flag;
             settings.count_rf |= SAM_FLAG | SAM_RNEXT;
             break;
         case 'F':
-            settings.flag_off |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.flag_off |= tmp_flag;
             settings.count_rf |= SAM_FLAG | SAM_RNEXT;
             break;
         case LONGOPT('g'):
-            settings.flag_anyon |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.flag_anyon |= tmp_flag;
             settings.count_rf |= SAM_FLAG | SAM_RNEXT;
             break;
         case 'G':
-            settings.flag_alloff |= bam_str2flag(optarg);
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.flag_alloff |= tmp_flag;
             settings.count_rf |= SAM_FLAG | SAM_RNEXT;
             break;
         case 'q':
@@ -1086,8 +1148,27 @@ int main_samview(int argc, char *argv[])
             }
             settings.count_rf = INT_MAX; // no way to know what we need
             break;
-        case LONGOPT('r'): settings.remove_flag |= bam_str2flag(optarg); break;
-        case LONGOPT('a'): settings.add_flag |= bam_str2flag(optarg); break;
+        case LONGOPT('r'):
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.remove_flag |= tmp_flag;
+            break;
+
+        case LONGOPT('a'):
+            tmp_flag = bam_str2flag(optarg);
+
+            if (tmp_flag < 0) {
+                print_error("view", "Unknown flag '%s'", optarg);
+                return 1;
+            }
+
+            settings.add_flag |= tmp_flag;
+            break;
 
         case 'x':
             if (*optarg == '^') {
@@ -1356,8 +1437,6 @@ int main_samview(int argc, char *argv[])
         }
     }
 
-    if ( settings.hts_idx ) hts_idx_destroy(settings.hts_idx);
-
     if (ga.write_index) {
         if (sam_idx_save(settings.out) < 0) {
             print_error_errno("view", "writing index failed");
@@ -1370,6 +1449,8 @@ int main_samview(int argc, char *argv[])
     }
 
 view_end:
+    if ( settings.hts_idx ) hts_idx_destroy(settings.hts_idx);
+
     if (settings.is_count && ret == 0) {
         if (fprintf(settings.fn_out? fp_out : samtools_stdout, "%" PRId64 "\n", settings.count) < 0) {
             if (settings.fn_out) print_error_errno("view", "writing to \"%s\" failed", settings.fn_out);
@@ -1460,9 +1541,10 @@ static int usage(FILE *fp, int exit_status, int is_long_help)
 "\n"
 "Filtering options (Only include in output reads that...):\n"
 "  -L, --target[s]-file FILE  ...overlap (BED) regions in FILE\n"
+"  -N, --qname-file [^]FILE   ...whose read name is listed in FILE (\"^\" negates)\n"
 "  -r, --read-group STR       ...are in read group STR\n"
-"  -R, --read-group-file FILE ...are in a read group listed in FILE\n"
-"  -N, --qname-file FILE      ...whose read name is listed in FILE\n"
+"  -R, --read-group-file [^]FILE\n"
+"                             ...are in a read group listed in FILE\n"
 "  -d, --tag STR1[:STR2]      ...have a tag STR1 (with associated value STR2)\n"
 "  -D, --tag-file STR:FILE    ...have a tag STR whose value is listed in FILE\n"
 "  -q, --min-MQ INT           ...have mapping quality >= INT\n"
@@ -1636,7 +1718,7 @@ int main_head(int argc, char *argv[])
     if (nrecords > 0) {
         b = bam_init1();
         uint64_t n;
-        int r;
+        int r = 0;
         for (n = 0; n < nrecords && (r = sam_read1(fp, hdr, b)) >= 0; n++) {
             if (sam_format1(hdr, b, &str) < 0) {
                 print_error_errno("head", "couldn't format record");
index 44783a974cc7f07aa00c68ee106f342567f960c8..eebfd6775597683992f33330970116cde3f034fc 100644 (file)
@@ -1,6 +1,6 @@
 /*  stats.c -- This is the former bamcheck integrated into samtools/htslib.
 
-    Copyright (C) 2012-2022 Genome Research Ltd.
+    Copyright (C) 2012-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
     Author: Sam Nicholls <sam@samnicholls.net>
@@ -766,6 +766,9 @@ static void collect_barcode_stats(bam1_t* bam_line, stats_t* stats) {
             continue;
 
         uint32_t barcode_len = strlen(barcode);
+        if (!barcode_len) {
+            continue;        //consider 0 size barcode same as no barcode - avoids issues with realloc below
+        }
         if (!stats->tags_barcode[tag].nbases) { // tag seen for the first time
             uint32_t offset = 0;
             for (i = 0; i < stats->ntags; i++)
@@ -2086,7 +2089,8 @@ static void init_group_id(stats_t *stats, stats_info_t *info, const char *id)
 }
 
 
-static void HTS_NORETURN error(const char *format, ...)
+static void  HTS_FORMAT(HTS_PRINTF_FMT, 1, 2) HTS_NORETURN
+error(const char *format, ...)
 {
     if ( !format )
     {
@@ -2129,7 +2133,7 @@ static void HTS_NORETURN error(const char *format, ...)
 
 void cleanup_stats_info(stats_info_t* info){
     if (info->fai) fai_destroy(info->fai);
-    sam_close(info->sam);
+    if (info->sam) sam_close(info->sam);
     free(info);
 }
 
@@ -2249,7 +2253,7 @@ int init_stat_info_fname(stats_info_t* info, const char* bam_fname, const htsFor
     return 0;
 }
 
-stats_t* stats_init()
+stats_t* stats_init(void)
 {
     stats_t *stats = calloc(1,sizeof(stats_t));
     if (!stats)
@@ -2437,14 +2441,34 @@ int main_stats(int argc, char *argv[])
         {"cov-threshold", required_argument, NULL, 'g'},
         {NULL, 0, NULL, 0}
     };
-    int opt;
+    int opt, tmp_flag;
 
     while ( (opt=getopt_long(argc,argv,"?hdsXxpr:c:l:i:t:m:q:f:F:g:I:S:P:@:",loptions,NULL))>0 )
     {
         switch (opt)
         {
-            case 'f': info->flag_require = bam_str2flag(optarg); break;
-            case 'F': info->flag_filter |= bam_str2flag(optarg); break;
+            case 'f':
+                tmp_flag = bam_str2flag(optarg);
+
+                if (tmp_flag < 0) {
+                    print_error("stats", "Unknown flag '%s'", optarg);
+                    return 1;
+                }
+
+                info->flag_require = tmp_flag;
+                break;
+
+            case 'F':
+                tmp_flag = bam_str2flag(optarg);
+
+                if (tmp_flag < 0) {
+                    print_error("stats", "Unknown flag '%s'", optarg);
+                    return 1;
+                }
+
+                info->flag_filter |= tmp_flag;
+                break;
+
             case 'd': info->flag_filter |= BAM_FDUP; break;
             case 'X': has_index_file = 1; break;
             case 's': break;
index b3462ccb20e9ef90f21dc51924b43e6458ef2481..6bd7946ba74317d623dddcb9459b0ab014057072 100644 (file)
@@ -2,7 +2,7 @@
 
 /*  stats.c -- This is the former bamcheck integrated into samtools/htslib.
 
-    Copyright (C) 2012-2022 Genome Research Ltd.
+    Copyright (C) 2012-2024 Genome Research Ltd.
 
     Author: Petr Danecek <pd3@sanger.ac.uk>
     Author: Sam Nicholls <sam@samnicholls.net>
@@ -768,6 +768,9 @@ static void collect_barcode_stats(bam1_t* bam_line, stats_t* stats) {
             continue;
 
         uint32_t barcode_len = strlen(barcode);
+        if (!barcode_len) {
+            continue;        //consider 0 size barcode same as no barcode - avoids issues with realloc below
+        }
         if (!stats->tags_barcode[tag].nbases) { // tag seen for the first time
             uint32_t offset = 0;
             for (i = 0; i < stats->ntags; i++)
@@ -2088,7 +2091,8 @@ static void init_group_id(stats_t *stats, stats_info_t *info, const char *id)
 }
 
 
-static void HTS_NORETURN error(const char *format, ...)
+static void  HTS_FORMAT(HTS_PRINTF_FMT, 1, 2) HTS_NORETURN
+error(const char *format, ...)
 {
     if ( !format )
     {
@@ -2131,7 +2135,7 @@ static void HTS_NORETURN error(const char *format, ...)
 
 void cleanup_stats_info(stats_info_t* info){
     if (info->fai) fai_destroy(info->fai);
-    sam_close(info->sam);
+    if (info->sam) sam_close(info->sam);
     free(info);
 }
 
@@ -2251,7 +2255,7 @@ int init_stat_info_fname(stats_info_t* info, const char* bam_fname, const htsFor
     return 0;
 }
 
-stats_t* stats_init()
+stats_t* stats_init(void)
 {
     stats_t *stats = calloc(1,sizeof(stats_t));
     if (!stats)
@@ -2439,14 +2443,34 @@ int main_stats(int argc, char *argv[])
         {"cov-threshold", required_argument, NULL, 'g'},
         {NULL, 0, NULL, 0}
     };
-    int opt;
+    int opt, tmp_flag;
 
     while ( (opt=getopt_long(argc,argv,"?hdsXxpr:c:l:i:t:m:q:f:F:g:I:S:P:@:",loptions,NULL))>0 )
     {
         switch (opt)
         {
-            case 'f': info->flag_require = bam_str2flag(optarg); break;
-            case 'F': info->flag_filter |= bam_str2flag(optarg); break;
+            case 'f':
+                tmp_flag = bam_str2flag(optarg);
+
+                if (tmp_flag < 0) {
+                    print_error("stats", "Unknown flag '%s'", optarg);
+                    return 1;
+                }
+
+                info->flag_require = tmp_flag;
+                break;
+
+            case 'F':
+                tmp_flag = bam_str2flag(optarg);
+
+                if (tmp_flag < 0) {
+                    print_error("stats", "Unknown flag '%s'", optarg);
+                    return 1;
+                }
+
+                info->flag_filter |= tmp_flag;
+                break;
+
             case 'd': info->flag_filter |= BAM_FDUP; break;
             case 'X': has_index_file = 1; break;
             case 's': break;
index 123f425e18dc366d4c0e2b36c7b58152db32ac37..81aebbfd686b589c95c68f554a6c0f4e1b229691 100644 (file)
@@ -43,7 +43,8 @@ DEALINGS IN THE SOFTWARE
 #include "htslib/sam.h"
 
 
-static void tmp_print_error(tmp_file_t *tmp, const char *fmt, ...) {
+static void HTS_FORMAT(HTS_PRINTF_FMT, 2, 3)
+tmp_print_error(tmp_file_t *tmp, const char *fmt, ...) {
     va_list argp;
 
     if (tmp->verbose) {
@@ -253,8 +254,8 @@ int tmp_file_write(tmp_file_t *tmp, bam1_t *inbam) {
         int ret;
 
         if ((ret = tmp_file_grow_ring_buffer(tmp, (tmp->offset + tmp->input_size + sizeof(bam1_t) + inbam->l_data) * 2))) {
-            tmp_print_error(tmp, "[tmp_file] Error: input line too big. (%ld).\n",
-                (tmp->input_size + inbam->l_data));
+            tmp_print_error(tmp, "[tmp_file] Error: input line too big. (%zu).\n",
+                            (tmp->input_size + inbam->l_data));
 
             return ret;
         }
@@ -405,7 +406,7 @@ int tmp_file_read(tmp_file_t *tmp, bam1_t *inbam) {
     tmp->entry_number++;
 
     if (tmp->read_size > tmp->output_size) {
-        tmp_print_error(tmp, "[tmp_file] Error: wrong size of data returned RS:%ld OS:%ld EN:%ld GS:%ld.\n",
+        tmp_print_error(tmp, "[tmp_file] Error: wrong size of data returned RS:%zu OS:%zu EN:%zu GS:%zu.\n",
             tmp->read_size, tmp->output_size, tmp->entry_number, tmp->group_size);
         return TMP_SAM_LZ4_ERROR;
     }
index 096933248eb828eba9303023ae248522e0b230f8..6cab2937911b0511e9834241f7f49d81c2f7a188 100644 (file)
@@ -45,7 +45,8 @@ DEALINGS IN THE SOFTWARE
 #include "htslib/sam.h"
 
 
-static void tmp_print_error(tmp_file_t *tmp, const char *fmt, ...) {
+static void HTS_FORMAT(HTS_PRINTF_FMT, 2, 3)
+tmp_print_error(tmp_file_t *tmp, const char *fmt, ...) {
     va_list argp;
 
     if (tmp->verbose) {
@@ -255,8 +256,8 @@ int tmp_file_write(tmp_file_t *tmp, bam1_t *inbam) {
         int ret;
 
         if ((ret = tmp_file_grow_ring_buffer(tmp, (tmp->offset + tmp->input_size + sizeof(bam1_t) + inbam->l_data) * 2))) {
-            tmp_print_error(tmp, "[tmp_file] Error: input line too big. (%ld).\n",
-                (tmp->input_size + inbam->l_data));
+            tmp_print_error(tmp, "[tmp_file] Error: input line too big. (%zu).\n",
+                            (tmp->input_size + inbam->l_data));
 
             return ret;
         }
@@ -407,7 +408,7 @@ int tmp_file_read(tmp_file_t *tmp, bam1_t *inbam) {
     tmp->entry_number++;
 
     if (tmp->read_size > tmp->output_size) {
-        tmp_print_error(tmp, "[tmp_file] Error: wrong size of data returned RS:%ld OS:%ld EN:%ld GS:%ld.\n",
+        tmp_print_error(tmp, "[tmp_file] Error: wrong size of data returned RS:%zu OS:%zu EN:%zu GS:%zu.\n",
             tmp->read_size, tmp->output_size, tmp->entry_number, tmp->group_size);
         return TMP_SAM_LZ4_ERROR;
     }
index 7d17aee189ee631985545c9c0e23533cb9f64392..618a098a68eb64c89d4b98a870c491d1c8f6509f 100755 (executable)
@@ -24,7 +24,7 @@
 # DEALINGS IN THE SOFTWARE.
 
 # Master version, for use in tarballs or non-git source copies
-VERSION=1.18
+VERSION=1.21
 
 # If we have a git clone, then check against the current tag
 if [ -e .git ]
index dbef939faa5d3c8bf0d0912ee48f2a467fdb531b..64093c2949065360ed25f2aab7fc5d11d05b38cc 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -186,6 +186,10 @@ def set_compiler_envvars():
             del os.environ[var]
 
 
+def format_macro_option(name, value):
+    return f"-D{name}={value}" if value is not None else f"-D{name}"
+
+
 def configure_library(library_dir, env_options=None, options=[]):
 
     configure_script = os.path.join(library_dir, "configure")
@@ -532,6 +536,9 @@ else:
 
 define_macros = []
 
+if os.environ.get("CIBUILDWHEEL", "0") == "1":
+    define_macros.append(("BUILDING_WHEEL", None))
+
 suffix = sysconfig.get_config_var('EXT_SUFFIX')
 
 internal_htslib_libraries = [
@@ -567,7 +574,8 @@ def prebuild_libchtslib(ext, force):
             # TODO Eventually by running configure here, we can set these
             # extra flags for configure instead of hacking on ALL_CPPFLAGS.
             args = " ".join(ext.extra_compile_args)
-            run_make(["ALL_CPPFLAGS=-I. " + args + " $(CPPFLAGS)", "lib-static"])
+            defines = " ".join([format_macro_option(*pair) for pair in ext.define_macros])
+            run_make(["ALL_CPPFLAGS=-I. " + args + " " + defines + " $(CPPFLAGS)", "lib-static"])
     else:
         log.warning("skipping 'libhts.a' (already built)")
 
index 2669df2f6c12c02cb78bf78bb36a70eb2baad05e..af0e270bb8e704e95d1b38b6e344aa3115c9cba8 100644 (file)
@@ -6,6 +6,7 @@ import collections
 import struct
 import copy
 import array
+from pysam import CDEL, CDIFF, CEQUAL, CINS, CMATCH, CPAD, CREF_SKIP, CSOFT_CLIP
 
 from TestUtils import (
     checkFieldEqual,
@@ -49,6 +50,38 @@ class TestAlignedSegment(ReadTest):
     and manipulated.
     """
 
+    def check_get_aligned_pairs_combos(self, a, exp):
+        def positions(exp):  return [(ppos, rpos)        for ppos, rpos, base, cigar in exp]
+        def with_seq(exp):   return [(ppos, rpos, base)  for ppos, rpos, base, cigar in exp]
+        def with_cigar(exp): return [(ppos, rpos, cigar) for ppos, rpos, base, cigar in exp]
+
+        self.assertEqual(a.get_aligned_pairs(), positions(exp))
+        self.assertEqual(a.get_aligned_pairs(with_seq=True), with_seq(exp))
+        self.assertEqual(a.get_aligned_pairs(with_cigar=True), with_cigar(exp))
+        self.assertEqual(a.get_aligned_pairs(with_seq=True, with_cigar=True), exp)
+
+        exp = [(ppos, rpos, base, cigar) for ppos, rpos, base, cigar in exp if ppos is not None and rpos is not None]
+
+        self.assertEqual(a.get_aligned_pairs(matches_only=True), positions(exp))
+        self.assertEqual(a.get_aligned_pairs(matches_only=True, with_seq=True), with_seq(exp))
+        self.assertEqual(a.get_aligned_pairs(matches_only=True, with_cigar=True), with_cigar(exp))
+        self.assertEqual(a.get_aligned_pairs(matches_only=True, with_seq=True, with_cigar=True), exp)
+
+    def check_get_aligned_pairs_combos_without_MD(self, a, exp):
+        def positions(exp): return [(ppos, rpos) for ppos, rpos, cigar in exp]
+
+        self.assertEqual(a.get_aligned_pairs(), positions(exp))
+        with self.assertRaises(ValueError): a.get_aligned_pairs(with_seq=True)
+        self.assertEqual(a.get_aligned_pairs(with_cigar=True), exp)
+        with self.assertRaises(ValueError): a.get_aligned_pairs(with_seq=True, with_cigar=True)
+
+        exp = [(ppos, rpos, cigar) for ppos, rpos, cigar in exp if ppos is not None and rpos is not None]
+
+        self.assertEqual(a.get_aligned_pairs(matches_only=True), positions(exp))
+        with self.assertRaises(ValueError): a.get_aligned_pairs(matches_only=True, with_seq=True)
+        self.assertEqual(a.get_aligned_pairs(matches_only=True, with_cigar=True), exp)
+        with self.assertRaises(ValueError): a.get_aligned_pairs(matches_only=True, with_seq=True, with_cigar=True)
+
     def testEmpty(self):
 
         a = pysam.AlignedSegment()
@@ -313,50 +346,50 @@ class TestAlignedSegment(ReadTest):
             ],
         )
 
-        self.assertEqual(
-            a.get_aligned_pairs(),
+        self.check_get_aligned_pairs_combos_without_MD(
+            a,
             [
-                (0, 20),
-                (1, 21),
-                (2, 22),
-                (3, 23),
-                (4, 24),
-                (5, 25),
-                (6, 26),
-                (7, 27),
-                (8, 28),
-                (9, 29),
-                (None, 30),
-                (10, 31),
-                (11, 32),
-                (12, 33),
-                (13, 34),
-                (14, 35),
-                (15, 36),
-                (16, 37),
-                (17, 38),
-                (18, 39),
-                (19, None),
-                (20, 40),
-                (21, 41),
-                (22, 42),
-                (23, 43),
-                (24, 44),
-                (25, 45),
-                (26, 46),
-                (27, 47),
-                (28, 48),
-                (29, 49),
-                (30, 50),
-                (31, 51),
-                (32, 52),
-                (33, 53),
-                (34, 54),
-                (35, 55),
-                (36, 56),
-                (37, 57),
-                (38, 58),
-                (39, 59),
+                (0, 20, CMATCH),
+                (1, 21, CMATCH),
+                (2, 22, CMATCH),
+                (3, 23, CMATCH),
+                (4, 24, CMATCH),
+                (5, 25, CMATCH),
+                (6, 26, CMATCH),
+                (7, 27, CMATCH),
+                (8, 28, CMATCH),
+                (9, 29, CMATCH),
+                (None, 30, CDEL),
+                (10, 31, CMATCH),
+                (11, 32, CMATCH),
+                (12, 33, CMATCH),
+                (13, 34, CMATCH),
+                (14, 35, CMATCH),
+                (15, 36, CMATCH),
+                (16, 37, CMATCH),
+                (17, 38, CMATCH),
+                (18, 39, CMATCH),
+                (19, None, CINS),
+                (20, 40, CMATCH),
+                (21, 41, CMATCH),
+                (22, 42, CMATCH),
+                (23, 43, CMATCH),
+                (24, 44, CMATCH),
+                (25, 45, CMATCH),
+                (26, 46, CMATCH),
+                (27, 47, CMATCH),
+                (28, 48, CMATCH),
+                (29, 49, CMATCH),
+                (30, 50, CMATCH),
+                (31, 51, CMATCH),
+                (32, 52, CMATCH),
+                (33, 53, CMATCH),
+                (34, 54, CMATCH),
+                (35, 55, CMATCH),
+                (36, 56, CMATCH),
+                (37, 57, CMATCH),
+                (38, 58, CMATCH),
+                (39, 59, CMATCH),
             ],
         )
 
@@ -435,40 +468,24 @@ class TestAlignedSegment(ReadTest):
     def test_get_aligned_pairs_soft_clipping(self):
         a = self.build_read()
         a.cigartuples = ((4, 2), (0, 35), (4, 3))
-        self.assertEqual(
-            a.get_aligned_pairs(),
-            [(0, None), (1, None)]
+        self.check_get_aligned_pairs_combos_without_MD(
+            a,
+            [(0, None, CSOFT_CLIP), (1, None, CSOFT_CLIP)]
             + [
-                (qpos, refpos)
+                (qpos, refpos, CMATCH)
                 for (qpos, refpos) in zip(range(2, 2 + 35), range(20, 20 + 35))
             ]
-            + [(37, None), (38, None), (39, None)],
-        )
-        self.assertEqual(
-            a.get_aligned_pairs(True),
-            # [(0, None), (1, None)] +
-            [
-                (qpos, refpos)
-                for (qpos, refpos) in zip(range(2, 2 + 35), range(20, 20 + 35))
-            ]
-            # [(37, None), (38, None), (39, None)]
+            + [(37, None, CSOFT_CLIP), (38, None, CSOFT_CLIP), (39, None, CSOFT_CLIP)],
         )
 
     def test_get_aligned_pairs_hard_clipping(self):
         a = self.build_read()
         a.cigartuples = ((5, 2), (0, 35), (5, 3))
-        self.assertEqual(
-            a.get_aligned_pairs(),
+        self.check_get_aligned_pairs_combos_without_MD(
+            a,
             # No seq, no seq pos
             [
-                (qpos, refpos)
-                for (qpos, refpos) in zip(range(0, 0 + 35), range(20, 20 + 35))
-            ],
-        )
-        self.assertEqual(
-            a.get_aligned_pairs(True),
-            [
-                (qpos, refpos)
+                (qpos, refpos, CMATCH)
                 for (qpos, refpos) in zip(range(0, 0 + 35), range(20, 20 + 35))
             ],
         )
@@ -476,23 +493,12 @@ class TestAlignedSegment(ReadTest):
     def test_get_aligned_pairs_skip(self):
         a = self.build_read()
         a.cigarstring = "2M100D38M"
-        self.assertEqual(
-            a.get_aligned_pairs(),
-            [(0, 20), (1, 21)]
-            + [(None, refpos) for refpos in range(22, 22 + 100)]
+        self.check_get_aligned_pairs_combos_without_MD(
+            a,
+            [(0, 20, CMATCH), (1, 21, CMATCH)]
+            + [(None, refpos, CDEL) for refpos in range(22, 22 + 100)]
             + [
-                (qpos, refpos)
-                for (qpos, refpos) in zip(
-                    range(2, 2 + 38), range(20 + 2 + 100, 20 + 2 + 100 + 38)
-                )
-            ],
-        )
-        self.assertEqual(
-            a.get_aligned_pairs(True),
-            [(0, 20), (1, 21)] +
-            # [(None, refpos) for refpos in range(21, 21+100)] +
-            [
-                (qpos, refpos)
+                (qpos, refpos, CMATCH)
                 for (qpos, refpos) in zip(
                     range(2, 2 + 38), range(20 + 2 + 100, 20 + 2 + 100 + 38)
                 )
@@ -502,17 +508,10 @@ class TestAlignedSegment(ReadTest):
     def test_get_aligned_pairs_match_mismatch(self):
         a = self.build_read()
         a.cigartuples = ((7, 20), (8, 20))
-        self.assertEqual(
-            a.get_aligned_pairs(),
-            [
-                (qpos, refpos)
-                for (qpos, refpos) in zip(range(0, 0 + 40), range(20, 20 + 40))
-            ],
-        )
-        self.assertEqual(
-            a.get_aligned_pairs(True),
+        self.check_get_aligned_pairs_combos_without_MD(
+            a,
             [
-                (qpos, refpos)
+                (qpos, refpos, CEQUAL if qpos < 20 else CDIFF)
                 for (qpos, refpos) in zip(range(0, 0 + 40), range(20, 20 + 40))
             ],
         )
@@ -522,8 +521,16 @@ class TestAlignedSegment(ReadTest):
         a.cigartuples = ((0, 1), (6, 1), (0, 1))
         # The padding operation is like an insertion into the reference.
         # See comment in test_get_aligned_pairs_padding_with_seq (below).
-        self.assertEqual(a.get_aligned_pairs(),
-                         [(0, 20), (1, None), (2, 21)])
+        self.check_get_aligned_pairs_combos_without_MD(a,
+                         [(0, 20, CMATCH), (1, None, CPAD), (2, 21, CMATCH)])
+
+    def test_get_aligned_pairs_padding_via_cigarstring(self):
+        a = self.build_read()
+        a.cigarstring = "1M1P1M"
+        # The padding operation is like an insertion into the reference.
+        # See comment in test_get_aligned_pairs_padding_with_seq (below).
+        self.check_get_aligned_pairs_combos_without_MD(a,
+                         [(0, 20, CMATCH), (1, None, CPAD), (2, 21, CMATCH)])
 
     def test_get_aligned_pairs_padding_with_seq(self):
         a = self.build_read()
@@ -550,80 +557,80 @@ class TestAlignedSegment(ReadTest):
         # string: "Alternatively, to describe the same alignments, we can
         # modify the reference sequence to contain pads that make room for
         # sequences inserted relative to the reference."
-        self.assertEqual(a.get_aligned_pairs(with_seq=True),
-                         [(0, 20, 'A'), (1, None, None), (2, 21, 'T')])
+        self.check_get_aligned_pairs_combos(a,
+            [(0, 20, "A", CMATCH), (1, None, None, CPAD), (2, 21, "T", CMATCH)])
 
     def test_get_aligned_pairs(self):
         a = self.build_read()
         a.query_sequence = "A" * 9
         a.cigarstring = "9M"
         a.set_tag("MD", "9")
-        self.assertEqual(
-            a.get_aligned_pairs(with_seq=True),
+        self.check_get_aligned_pairs_combos(
+            a,
             [
-                (0, 20, "A"),
-                (1, 21, "A"),
-                (2, 22, "A"),
-                (3, 23, "A"),
-                (4, 24, "A"),
-                (5, 25, "A"),
-                (6, 26, "A"),
-                (7, 27, "A"),
-                (8, 28, "A"),
+                (0, 20, "A", CMATCH),
+                (1, 21, "A", CMATCH),
+                (2, 22, "A", CMATCH),
+                (3, 23, "A", CMATCH),
+                (4, 24, "A", CMATCH),
+                (5, 25, "A", CMATCH),
+                (6, 26, "A", CMATCH),
+                (7, 27, "A", CMATCH),
+                (8, 28, "A", CMATCH),
             ],
         )
 
         a.set_tag("MD", "4C4")
-        self.assertEqual(
-            a.get_aligned_pairs(with_seq=True),
+        self.check_get_aligned_pairs_combos(
+            a,
             [
-                (0, 20, "A"),
-                (1, 21, "A"),
-                (2, 22, "A"),
-                (3, 23, "A"),
-                (4, 24, "c"),
-                (5, 25, "A"),
-                (6, 26, "A"),
-                (7, 27, "A"),
-                (8, 28, "A"),
+                (0, 20, "A", CMATCH),
+                (1, 21, "A", CMATCH),
+                (2, 22, "A", CMATCH),
+                (3, 23, "A", CMATCH),
+                (4, 24, "c", CMATCH),
+                (5, 25, "A", CMATCH),
+                (6, 26, "A", CMATCH),
+                (7, 27, "A", CMATCH),
+                (8, 28, "A", CMATCH),
             ],
         )
 
         a.cigarstring = "5M2D4M"
         a.set_tag("MD", "4C^TT4")
-        self.assertEqual(
-            a.get_aligned_pairs(with_seq=True),
+        self.check_get_aligned_pairs_combos(
+            a,
             [
-                (0, 20, "A"),
-                (1, 21, "A"),
-                (2, 22, "A"),
-                (3, 23, "A"),
-                (4, 24, "c"),
-                (None, 25, "T"),
-                (None, 26, "T"),
-                (5, 27, "A"),
-                (6, 28, "A"),
-                (7, 29, "A"),
-                (8, 30, "A"),
+                (0, 20, "A", CMATCH),
+                (1, 21, "A", CMATCH),
+                (2, 22, "A", CMATCH),
+                (3, 23, "A", CMATCH),
+                (4, 24, "c", CMATCH),
+                (None, 25, "T", CDEL),
+                (None, 26, "T", CDEL),
+                (5, 27, "A", CMATCH),
+                (6, 28, "A", CMATCH),
+                (7, 29, "A", CMATCH),
+                (8, 30, "A", CMATCH),
             ],
         )
 
         a.cigarstring = "5M2D2I2M"
         a.set_tag("MD", "4C^TT2")
-        self.assertEqual(
-            a.get_aligned_pairs(with_seq=True),
+        self.check_get_aligned_pairs_combos(
+            a,
             [
-                (0, 20, "A"),
-                (1, 21, "A"),
-                (2, 22, "A"),
-                (3, 23, "A"),
-                (4, 24, "c"),
-                (None, 25, "T"),
-                (None, 26, "T"),
-                (5, None, None),
-                (6, None, None),
-                (7, 27, "A"),
-                (8, 28, "A"),
+                (0, 20, "A", CMATCH),
+                (1, 21, "A", CMATCH),
+                (2, 22, "A", CMATCH),
+                (3, 23, "A", CMATCH),
+                (4, 24, "c", CMATCH),
+                (None, 25, "T", CDEL),
+                (None, 26, "T", CDEL),
+                (5, None, None, CINS),
+                (6, None, None, CINS),
+                (7, 27, "A", CMATCH),
+                (8, 28, "A", CMATCH),
             ],
         )
 
@@ -643,54 +650,21 @@ class TestAlignedSegment(ReadTest):
         a.cigarstring = "5M1N5M"
         a.set_tag("MD", "10")
 
-        self.assertEqual(
-            a.get_aligned_pairs(with_seq=True),
-            [
-                (0, 20, "A"),
-                (1, 21, "A"),
-                (2, 22, "A"),
-                (3, 23, "A"),
-                (4, 24, "A"),
-                (None, 25, None),
-                (5, 26, "A"),
-                (6, 27, "A"),
-                (7, 28, "A"),
-                (8, 29, "A"),
-                (9, 30, "A"),
-            ],
-        )
-
-        self.assertEqual(
-            a.get_aligned_pairs(with_seq=False),
-            [
-                (0, 20),
-                (1, 21),
-                (2, 22),
-                (3, 23),
-                (4, 24),
-                (None, 25),
-                (5, 26),
-                (6, 27),
-                (7, 28),
-                (8, 29),
-                (9, 30),
-            ],
-        )
-
-        self.assertEqual(
-            a.get_aligned_pairs(matches_only=True, with_seq=False),
+        self.check_get_aligned_pairs_combos(
+            a,
             [
-                (0, 20),
-                (1, 21),
-                (2, 22),
-                (3, 23),
-                (4, 24),
-                (5, 26),
-                (6, 27),
-                (7, 28),
-                (8, 29),
-                (9, 30),
-            ],
+                (0, 20, "A", CMATCH),
+                (1, 21, "A", CMATCH),
+                (2, 22, "A", CMATCH),
+                (3, 23, "A", CMATCH),
+                (4, 24, "A", CMATCH),
+                (None, 25, None, CREF_SKIP),
+                (5, 26, "A", CMATCH),
+                (6, 27, "A", CMATCH),
+                (7, 28, "A", CMATCH),
+                (8, 29, "A", CMATCH),
+                (9, 30, "A", CMATCH),
+            ]
         )
 
     def test_equivalence_matches_only_and_with_seq(self):
@@ -698,32 +672,22 @@ class TestAlignedSegment(ReadTest):
         a.query_sequence = "ACGT" * 2
         a.cigarstring = "4M1D4M"
         a.set_tag("MD", "4^x4")
-        full = (
-            list(zip(range(0, 4), range(20, 24), "ACGT"))
-            + [(None, 24, "x")]
-            + list(zip(range(4, 8), range(25, 29), "ACGT"))
-        )
-        self.assertEqual(a.get_aligned_pairs(matches_only=False, with_seq=True), full)
-
-        self.assertEqual(
-            a.get_aligned_pairs(matches_only=True, with_seq=True),
-            [x for x in full if x[0] is not None and x[1] is not None],
+        self.check_get_aligned_pairs_combos(
+            a,
+            list(zip(range(0, 4), range(20, 24), "ACGT", [CMATCH] * 4))
+            + [(None, 24, "x", CDEL)]
+            + list(zip(range(4, 8), range(25, 29), "ACGT", [CMATCH] * 4)),
         )
 
         a = self.build_read()
         a.query_sequence = "ACGT" * 2
         a.cigarstring = "4M1N4M"
         a.set_tag("MD", "8")
-        full = (
-            list(zip(range(0, 4), range(20, 24), "ACGT"))
-            + [(None, 24, None)]
-            + list(zip(range(4, 8), range(25, 29), "ACGT"))
-        )
-        self.assertEqual(a.get_aligned_pairs(matches_only=False, with_seq=True), full)
-
-        self.assertEqual(
-            a.get_aligned_pairs(matches_only=True, with_seq=True),
-            [x for x in full if x[0] is not None and x[1] is not None],
+        self.check_get_aligned_pairs_combos(
+            a,
+            list(zip(range(0, 4), range(20, 24), "ACGT", [CMATCH] * 4))
+            + [(None, 24, None, 3)]
+            + list(zip(range(4, 8), range(25, 29), "ACGT", [CMATCH] * 4)),
         )
 
     def test_get_aligned_pairs_lowercase_md(self):
@@ -1069,7 +1033,7 @@ class TestBaseModifications(unittest.TestCase):
         expect = {
             ("C", 0, "m"): [(6, 102), (17, 128), (20, 153), (31, 179), (34, 204)],
             ("N", 0, "n"): [(15, 212)],
-            ("C", 0, 76792): [(19, 161), (34, 187)],
+            ("C", 0, 76792): [(19, 161), (34, 33)],
         }
 
         with pysam.AlignmentFile(filename, check_sq=False) as inf:
@@ -1090,6 +1054,21 @@ class TestBaseModifications(unittest.TestCase):
             r = next(iter(inf))
             self.assertDictEqual(r.modified_bases, expect)
 
+    def testExplicit(self):
+        """reference bases should always be the same nucleotide
+        """
+        filename = os.path.join(BAM_DATADIR, "MM-explicit.bam")
+        expected_output = [
+            {("C", 0, "m"): [(9, 200), (10, 50), (14, 160)], ("C", 0, "h"): [(9, 10), (10, 170), (14, 20)]},
+            {("C", 0, "m"): [(9, 200), (10, 50), (13, 10), (14, 160), (16, 10)],
+             ("C", 0, "h"): [(9, 10), (10, 170), (13, 5), (14, 20), (16, 5)]},
+            {("C", 0, "m"): [(9, 200), (14, 160)], ("C", 0, "h"): [(9, 10), (10, 170), (13, 5), (14, 20), (16, 5)]},
+        ]
+
+        with pysam.AlignmentFile(filename, check_sq=False) as inf:
+            for r, expected in zip(inf, expected_output):
+                self.assertDictEqual(r.modified_bases, expected)
+
     def testMulti(self):
         """reference bases should always be the same nucleotide
         """
@@ -1609,6 +1588,18 @@ class TestSetTagGetTag(ReadTest):
             alt_value_type="I",
         )
 
+    def test_set_tag_invalid_value_type(self):
+        with self.assertRaises(ValueError):
+            self.check_tag("TT", "abc", value_type="#")
+
+    def test_set_array_tag_invalid_value_type(self):
+        with self.assertRaises(ValueError):
+            self.check_tag("TT", array.array('I', range(4)), value_type='#')
+
+    def test_set_array_tag_invalid_typecode(self):
+        with self.assertRaises(ValueError):
+            self.check_tag("TT", array.array('L', range(4)), value_type=None)
+
 
 class TestSetTagsGetTag(TestSetTagGetTag):
     def check_tag(self, tag, value, value_type, alt_value_type=None):
index 91e044c202ee78733460ae20f7f350fd8d986963..a3d97148700f24beef648dc91dabb628415d1af5 100644 (file)
@@ -286,7 +286,6 @@ class TestHeaderWriteRead(unittest.TestCase):
     def check_read_write(self, flag_write, header):
 
         fn = get_temp_filename()
-        print(fn)
         with pysam.AlignmentFile(
                 fn,
                 flag_write,
@@ -294,12 +293,13 @@ class TestHeaderWriteRead(unittest.TestCase):
                 reference_filename=os.path.join(BAM_DATADIR, "ex1.fa")) as outf:
             a = pysam.AlignedSegment()
             a.query_name = "abc"
+            a.flag = pysam.FUNMAP
             outf.write(a)
 
         with pysam.AlignmentFile(fn) as inf:
             read_header = inf.header
 
-        # os.unlink(fn)
+        os.unlink(fn)
         self.compare_headers(header, read_header)
         expected_lengths = dict([(x["SN"], x["LN"]) for x in header["SQ"]])
         self.assertEqual(expected_lengths,
index cde197e6d1bdd0c0bb4c773e5c5bccea5dda6ddf..11944c26a4380a0fe64c25872935d5aa4a32f2ba 100644 (file)
@@ -5,6 +5,8 @@ import contextlib
 import inspect
 import subprocess
 import tempfile
+import time
+
 import pysam
 
 WORKDIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
@@ -239,15 +241,27 @@ def get_temp_context(suffix="", keep=False):
 
 
 def make_data_files(directory):
-    what = None
+    if os.path.exists(os.path.join(directory, 'all.stamp')):
+        return
+
+    make = os.environ.get('MAKE', 'make')
+
+    for attempt in range(1, 6):
+        try:
+            os.mkdir(os.path.join(directory, 'all.lock'), 0o700)
+            break
+        except FileExistsError:
+            time.sleep(attempt)
+            continue
+    else:
+        raise RuntimeError(f'Directory {directory!r} already locked: try `{make} clean` there')
+
     try:
-        if not os.path.exists(os.path.join(directory, "all.stamp")):
-            subprocess.check_output(["make", "-C", directory], stderr=subprocess.STDOUT)
+        subprocess.check_output([make, '-C', directory], stderr=subprocess.STDOUT, encoding='ascii')
     except subprocess.CalledProcessError as e:
-        what = "Making test data in '%s' failed:\n%s" % (directory, force_str(e.output))
-
-    if what is not None:
-        raise RuntimeError(what)
+        raise RuntimeError(f'Making test data in {directory!r} failed:\n{e.output}') from None
+    finally:
+        os.rmdir(os.path.join(directory, 'all.lock'))
 
 
 def load_and_convert(filename, encode=True):
index 9663ea6a228b5886af995abfcfd944176a698af4..ba67a4945d92f5bcc20872d4953d3dc712144ba3 100644 (file)
@@ -23,7 +23,7 @@ def genomes_data():
     fn_small = "small.vcf.gz"
     if not os.path.exists(fn_small):
         os.system("bcftools view {} | head -n 10000 | bgzip > {}".format(fn, fn_small))
-        os.system("tabix -p vcf {}".format(fn_small))
+        os.system("tabix -f -p vcf {}".format(fn_small))
         
     return fn_small
 
index 310b83839b6b0f2a62e08a93d1cebf001eb8d174..9b59fb9f705c27a3418dab2a16dd523061a9ffba 100644 (file)
@@ -88,3 +88,23 @@ def test_set_sample_alleles(vcf_header):
 
     with pytest.raises(ValueError, match='Use .allele_indices to set integer allele indices'):
         record.samples['sample1'].alleles = (1, 0)
+
+
+def test_repeated_new_record(vcf_header):
+    vcf_header.formats.add('GT', 1, 'String', "Genotype")
+    vcf_header.formats.add("AA", 1, "String", "An annotation")
+    vcf_header.formats.add("BB", 1, "String", "Another annotation")
+
+    data = {'id': 'INS_1', 'contig': '1', 'start': 10, 'stop': 15, 'alleles': ['A', 'TCGA'],
+            'samples': [{'AA': ('one'), 'GT': (0, 1), 'BB': ('two')},
+                        {'GT': (1, 0), 'BB': ('three')}]}
+
+    record1 = vcf_header.new_record(**data)
+    assert '\tGT:' in str(record1)  # Verify that GT is output first
+    assert record1.samples['sample1'].alleles == ('A', 'TCGA')
+    assert record1.samples['sample2'].alleles == ('TCGA', 'A')
+
+    record2 = vcf_header.new_record(**data)
+    assert '\tGT:' in str(record2)  # Verify that GT is actually emitted and is output first
+    assert record2.samples['sample1'].alleles == ('A', 'TCGA')
+    assert record2.samples['sample2'].alleles == ('TCGA', 'A')
index 9c3fe757328248b67bb4f1a1988f1b98b5e3abe2..8a155fcad6cd5b7e4ac74e06b0935679d0e4ca6b 100644 (file)
@@ -11,15 +11,15 @@ all.stamp: $(VCFGZ) $(BCF)
 
 %.vcf.gz: %.vcf
        bgzip < $< > $@
-       tabix -p vcf $@    # create tbi index
-       bcftools index $@  # create csi index
+       tabix -f -p vcf $@    # create tbi index
+       bcftools index -f $@  # create csi index
 
 %.bcf: %.vcf.gz
        bcftools view -O b $< -o $@
-       bcftools index $@
+       bcftools index -f $@
 
 example_empty.bcf: example_empty.vcf.gz
        touch $@
 
 clean:
-       -rm -f all.stamp *.gz *.tbi *.csi *.bcf
+       -rm -rf all.lock all.stamp *.gz *.tbi *.csi *.bcf
index 62920ecc1fdaed7e5e723ef287b7267aa6227a50..475a7d59949a785b796ed2176e182a9063bac80d 100644 (file)
@@ -1,2 +1,2 @@
 @CO    Separate m, h and N modifications
-*      0       *       0       0       *       *       0       0       AGCTCTCCAGAGTCGNACGCCATYCGCGCGCCACCA    *       Mm:Z:C+m,2,2,1,4,1;C+76792,6,7;N+n,15;  Ml:B:C,102,128,153,179,204,161,187,212,169
+*      0       *       0       0       *       *       0       0       AGCTCTCCAGAGTCGNACGCCATYCGCGCGCCACCA    *       Mm:Z:C+m,2,2,1,4,1;C+76792,6,7;N+n,15;  Ml:B:C,102,128,153,179,204,161,33,212
diff --git a/tests/pysam_data/MM-explicit.sam b/tests/pysam_data/MM-explicit.sam
new file mode 100644 (file)
index 0000000..c230a9d
--- /dev/null
@@ -0,0 +1,27 @@
+@CO    Testing explicit vs implicit base modifications.
+@CO    This covers the case where a lack of a signal could be either
+@CO    implicitly assumed to be no-mod (default) or assumed to be
+@CO    unchecked and require an explicit statement to indicate it was
+@CO    looked at and no base modification was observed.
+@CO    
+@CO    ATCATCATTCCTACCGCTATAGCCT  r1; implicit
+@CO      -  -   ..  -. -     --
+@CO             Mm   M
+@CO      -  -   ..  -. -     --
+@CO             hH   h
+@CO    
+@CO    ATCATCATTCCTACCGCTATAGCCT  r2; explicit to a small region
+@CO      -  -   ??  ?? ?     --
+@CO             Mm  mM m
+@CO      -  -   ??  ?? ?     --
+@CO             hH  hh h
+@CO    
+@CO    ATCATCATTCCTACCGCTATAGCCT  r3; mixture
+@CO      -  -   .   -. -     --
+@CO             M    M
+@CO      -  -   ??  ?? ?     --
+@CO             hH  hh h     --
+@CO    
+r1     0       *       0       0       *       *       0       0       ATCATCATTCCTACCGCTATAGCCT       *       Mm:Z:C+mh,2,0,1;        Ml:B:C,200,10,50,170,160,20
+r2     0       *       0       0       *       *       0       0       ATCATCATTCCTACCGCTATAGCCT       *       Mm:Z:C+mh?,2,0,0,0,0;   Ml:B:C,200,10,50,170,10,5,160,20,10,5
+r3     0       *       0       0       *       *       0       0       ATCATCATTCCTACCGCTATAGCCT       *       Mm:Z:C+m.,2,2;C+h?,2,0,0,0,0;   Ml:B:C,200,160,10,170,5,20,5
index b2259a09e41801afe5450df0f160e16235231c07..ac2831bc119d80cb121e834896e00d3ae312008e 100644 (file)
@@ -3,5 +3,5 @@
 @CO    r2 has them combined together, for example as produced by
 @CO    a joint basecaller which assigns probabilities to all
 @CO    trained events simultaneously.
-r1     0       *       0       0       *       *       0       0       AGCTCTCCAGAGTCGNACGCCATYCGCGCGCCACCA    *       Mm:Z:C+m,2,2,1,4,1;C+h,6,7;N+n,15,2;    Ml:B:C,128,153,179,204,230,159,6,215,240
+r1     0       *       0       0       *       *       0       0       AGCTCTCCAGAGTCGNACGCCATYCGCGCGCCACCA    *       Mm:Z:C+m,2,2,1,4,1;C+h,6,7;N+n,15,2;    Ml:B:C,128,153,179,204,230,159,6,215,240        MN:i:36
 r2     0       *       0       0       *       *       0       0       AGCTCTCCAGAGTCGNACGCCATYCGCGCGCCACCA    *       Mm:Z:C+mh,2,2,0,0,4,1;N+n,15;   Ml:B:C,77,159,103,133,128,108,154,82,179,57,204,31,240
index d87044064112e22a9109576f60f975a267030a8d..16d533adf0bceeb085baf7ae2cc8d8a944b4beb2 100644 (file)
@@ -100,8 +100,8 @@ explicit_index.cram: ex1.cram
        cp ex1.cram $@
 
 clean:
-       rm -fr [a-z]*.bam *.bai *.csi *.fai *.gzi *.pileup* [a-z]*.cram *.crai \
-       all.stamp *~ calDepth *.dSYM pysam_*.sam \
+       rm -fr [A-Za-z]*.bam *.bai *.csi *.fai *.gzi *.pileup* [A-Za-z]*.cram *.crai \
+       all.lock all.stamp *~ calDepth *.dSYM pysam_*.sam \
        ex2.sam ex2.sam.gz ex1.sam ex1.fa.gz \
        with_md.sam.gz \
        *.fq.gz
index 6d49a94f3c40565da5b0f75a7134adac6c8ffece..182e919f53246c2973e2e2a2b90bbecb933f7332 100644 (file)
@@ -83,7 +83,8 @@ class SamtoolsTest(unittest.TestCase):
         "index ex1.bam %(out)s_ex1.bam.fai",
         "index -@2 ex1.bam %(out)s_ex1.bam.fai",
         "idxstats ex1.bam > %(out)s_ex1.idxstats",
-        "fixmate ex1.bam %(out)s_ex1.fixmate.bam",
+        # TODO: fixmate behaviour changed in 1.21
+        #"fixmate ex1.bam %(out)s_ex1.fixmate.bam",
         "flagstat ex1.bam > %(out)s_ex1.flagstat",
         "calmd ex1.bam ex1.fa > %(out)s_ex1.calmd.bam",
         # use -s option, otherwise the following error in samtools 1.2:
@@ -178,8 +179,6 @@ class SamtoolsTest(unittest.TestCase):
 
         command = self.get_command(statement)
 
-        # self.assertTrue(command in pysam.SAMTOOLS_DISPATCH)
-
         targets = [x for x in parts if "%(out)s" in x]
         samtools_targets = [x % r_samtools for x in targets]
         pysam_targets = [x % r_pysam for x in targets]
index 19812df06e217e485bcc72db90976e2f56cd3c66..4927586b794b01eeeeacb016e9a8168c63b67aa8 100644 (file)
@@ -25,16 +25,16 @@ all.stamp: $(DERIVED_FILES)
        bgzip -c $< > $@
 
 %.gff2.gz.tbi: %.gff2.gz
-       tabix -p gff $<
+       tabix -f -p gff $<
 
 %.gff3.gz.tbi: %.gff3.gz
-       tabix -p gff $<
+       tabix -f -p gff $<
 
 %.gtf.gz.tbi: %.gtf.gz
-       tabix -p gff $<
+       tabix -f -p gff $<
 
 %.gz.tbi: %.gz
-       tabix -p $(subst .,,$(suffix $*)) $<
+       tabix -f -p $(subst .,,$(suffix $*)) $<
 
 clean:
-       -rm -f all.stamp $(DERIVED_FILES)
+       -rm -rf all.lock all.stamp $(DERIVED_FILES)
diff --git a/tests/typechecking_test.py b/tests/typechecking_test.py
new file mode 100644 (file)
index 0000000..5dce485
--- /dev/null
@@ -0,0 +1,135 @@
+import inspect
+import os
+import pytest
+import re
+from typing import TYPE_CHECKING
+
+import pysam
+import pysam.samtools
+import pysam.bcftools
+
+from TestUtils import BAM_DATADIR, make_data_files
+
+try:
+    import mypy.api
+except ImportError:
+    pytest.skip('mypy API not available', allow_module_level=True)
+
+PREAMBLE = """
+import re
+from typing import TYPE_CHECKING
+
+import pysam
+import pysam.samtools
+import pysam.bcftools
+
+def typecheck(check_locals = True): pass
+"""
+
+MYPY_OPTIONS = ['--no-incremental', '--no-error-summary', '--follow-imports', 'silent']
+
+
+def typecheck(check_locals: bool = True):
+    myframe = inspect.currentframe()
+    if not myframe: pytest.skip('current stack frame not available')
+    caller = myframe.f_back
+    if not caller: pytest.skip('caller stack frame not available')
+
+    code = inspect.getsource(caller)
+    stdout, stderr, status = mypy.api.run(MYPY_OPTIONS + ['--command', PREAMBLE + code])
+    assert status == 0, f'mypy failed:\n{stdout}{stderr}'
+
+    types = {}
+    for line in stdout.splitlines():
+        m = re.search(r'note:   *(\w+): ([\w.]*)', line)
+        if m: types[m.group(1)] = m.group(2)
+
+    def _plain(s):
+        s = re.sub(r"<class '([^']*)'>", r'\1', s)
+        s = re.sub(r'builtins\.', '', s)
+        return s
+
+    if check_locals:
+        for var, vartype in types.items():
+            assert _plain(vartype) == _plain(repr(type(caller.f_locals[var]))), f'Incorrect type for {var!r}'
+
+    return types
+
+
+def setUpModule():
+    make_data_files(BAM_DATADIR)
+
+
+@pytest.fixture
+def aln():
+    header = pysam.AlignmentHeader.from_references(['chr1', 'chr2'], [50000, 20000])
+    a = pysam.AlignedSegment(header)
+    a.query_name = 'read_one'
+    a.flag = pysam.FPAIRED | pysam.FREAD1
+    a.reference_id = 0
+    a.reference_start = 1000
+    a.mapping_quality = 20
+    a.next_reference_id = 1
+    a.next_reference_start = 5000
+    a.template_length = 0
+    a.cigartuples = ((pysam.CMATCH, 6), (pysam.CINS, 4), (pysam.CMATCH, 2))
+    a.query_sequence = 'ATGCATGCATGC'
+    a.query_qualities = pysam.qualitystring_to_array('abcdefghijkl')
+    return a
+
+
+@pytest.fixture
+def sam_fname():
+    return os.path.join(BAM_DATADIR, 'ex3.sam')
+
+
+@pytest.fixture
+def bam_fname():
+    return os.path.join(BAM_DATADIR, 'ex1.bam')
+
+
+def test_AlignedSegment_is_mapped(aln: pysam.AlignedSegment) -> None:
+    rmap = aln.is_mapped
+    mmap = aln.mate_is_mapped
+    rfwd = aln.is_forward
+    mfwd = aln.mate_is_forward
+
+    if TYPE_CHECKING: reveal_locals()
+    types = typecheck()
+
+
+def test_AlignmentHeader_get_reference_length(sam_fname: str) -> None:
+    inf = pysam.AlignmentFile(sam_fname)
+    n1 = inf.get_reference_length('chr1')
+    hdr = inf.header
+    n2 = hdr.get_reference_length('chr1')
+
+    if TYPE_CHECKING: reveal_locals()
+    types = typecheck()
+
+
+def test_pileup_iterator_column(bam_fname: str) -> None:
+    inf = pysam.AlignmentFile(bam_fname)
+    pu = inf.pileup('chr1')
+    for p in pu:
+        pid = p.reference_id
+        ppos = p.reference_pos
+
+    if TYPE_CHECKING: reveal_locals()
+    types = typecheck(check_locals=False)
+    assert re.search(r'\.IteratorColumn(|All|AllRefs|Region)$', types['pu'])
+    assert types['p'].endswith('PileupColumn')
+    assert types['pid'] == 'builtins.int'
+    assert types['ppos'] == 'builtins.int'
+
+
+def test_samtools_subcommands() -> None:
+    p1_samtools_faidx = pysam.samtools.faidx
+    p2_faidx = pysam.faidx
+    p3_view = pysam.view
+    p4_bcftools_view = pysam.bcftools.view
+
+    if TYPE_CHECKING: reveal_locals()
+    types = typecheck()
+    for var, vartype in types.items():
+        assert vartype.endswith('PysamDispatcher'), f'{var!r} is not a dispatcher'