Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions src/conversion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,12 @@ extern "C" {
#include <htslib/tbx.h>
}

// Run a bcftools function in a forked child process. bcftools main_* use global optind
// and other process-wide state; forking gives each call its own address space for safe
// parallelism.
// Run a bcftools function in a forked child. bcftools main_* use global optind and other
// process-wide state; forking gives each call its own address space for safe parallelism.
static int run_bcftools_in_fork(int (*func)(int, char**), int argc, char** argv, bool silenceStderr = true) {
// Serialize the forks. fork() from a TBB worker (batch mode, --threads > 1)
// clones only the calling thread; if another worker holds a stdio/htslib lock
// at fork time, the child can deadlock acquiring it (the owner doesn't exist in
// the child). glibc/macOS already make malloc fork-safe via atfork handlers, so
// the remaining exposure is two bcftools children running concurrently. This
// lock allows at most one bcftools fork in flight at a time.
// Serialize forks: fork() from a TBB worker clones only the calling thread, so a lock
// another worker holds (stdio/htslib) can deadlock the child. This mutex bounds it to one
// bcftools fork in flight (malloc is already fork-safe via atfork).
static std::mutex forkMutex;
std::lock_guard<std::mutex> forkLock(forkMutex);

Expand Down Expand Up @@ -313,11 +309,8 @@ static bam1_t* build_bam_from_result(const std::string& qname_full,
qname.resize(qname.size() - 2);
}

// R2 of a pair is reverse-complemented upstream (readFastqPaired) before alignment,
// so the aligner's rev bit for it is inverted relative to the original fragment.
// Report the original read's true strand in the FLAG and TLEN; the stored SEQ/CIGAR
// stay in the aligned (forward-reference) orientation, which is what BAM expects for
// either strand.
// R2 was reverse-complemented upstream, so the aligner's rev bit is inverted vs the original
// fragment. Report true strand in FLAG/TLEN; SEQ/CIGAR stay forward-reference, as BAM expects.
const uint8_t effective_rev = (is_paired && !is_read1) ? static_cast<uint8_t>(!aln->rev) : aln->rev;
uint16_t flag = compute_sam_flags(is_paired, is_read1, effective_rev, mate_rev, proper_frag, mate_unmapped);

Expand Down
6 changes: 2 additions & 4 deletions src/gap_map_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,8 @@ void invertGapMap(std::map<T, T>& gapMap,
// start inside of a range
auto curIt = leftIt;
if (static_cast<int64_t>(end) <= static_cast<int64_t>(curIt->second)) {
// [start, end] lies entirely within one gap run; reversing an all-gap span
// is a no-op. Without this clamp, the walk below appended a run to the gap's
// end plus a negative-length run, corrupting the gap map (e.g. inverting
// [10,20] inside {0:30} dropped [21,30]).
// [start,end] within one gap run: reversing an all-gap span is a no-op; without this
// clamp the walk emits a negative-length run (inverting [10,20] inside {0:30} dropped [21,30]).
blockRuns.emplace_back(true,
std::make_pair(static_cast<int64_t>(start), static_cast<int64_t>(end)));
} else {
Expand Down
12 changes: 5 additions & 7 deletions src/genotyping.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,8 @@ std::string genotyping::applyMutationSpectrum(const std::string& line,

gls[0] += scaled_submat[ref_nuc_idx][ref_nuc_idx];
for (size_t i = 1; i < gls.size(); i++) {
// scaled_submat is the 4x4 A/C/G/T substitution-spectrum prior, so apply
// it only to base ALTs. A non-base ALT (e.g. '*' spanning deletion) has
// index > 3 and would over-read the row, so leave its likelihood
// unmodified. Matches how indels use the unmodified bcftools posterior.
// scaled_submat is the 4x4 A/C/G/T substitution prior; apply only to base ALTs. A
// non-base ALT (e.g. '*') has index >3 and would over-read the row, so leave it as-is.
int alt_idx = getIndexFromNucleotide(alts[i - 1]);
if (alt_idx <= 3) {
gls[i] = gls[i] + scaled_submat[ref_nuc_idx][alt_idx];
Expand All @@ -264,9 +262,9 @@ std::string genotyping::applyMutationSpectrum(const std::string& line,

if (min_gl_index == 0) return "";

// Consensus gate: only emit an ALT that is the majority allele and has at least
// minDepth high-quality reads. Guards against bcftools (and, at low coverage, the
// spectrum prior) emitting sub-majority or shallow ALTs as false positives.
// Consensus gate: emit an ALT only if it's the majority allele with >= minDepth
// high-quality reads. Guards against bcftools / the low-coverage spectrum prior
// emitting sub-majority or shallow false-positive ALTs.
if (!passesConsensusGate(min_gl_index, ads, minDepth)) return "";

gt = min_gl_index;
Expand Down
9 changes: 4 additions & 5 deletions src/genotyping.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ void fillMutationMatricesFromFile(mutationMatrices& mutMat, std::ifstream& inf);
std::string applyMutationSpectrum(const std::string& line, const std::vector<std::vector<double>>& scaled_submat,
int minDepth, double minQual);

// Field-standard consensus gate for haploid genotyping: the called allele (calledIdx
// into AD; 0 = ref) must (a) be the majority — strictly more high-quality reads than
// all other alleles combined — and (b) be supported by at least minDepth high-quality
// reads. Below that, a position should be a no-call (ref/N), not a variant. Returns
// true (no filtering) only when AD is missing/uninformative.
// Consensus gate for haploid genotyping: the called allele (calledIdx into AD; 0 = ref)
// must be the majority (strictly more high-quality reads than all others combined) and
// have >= minDepth high-quality reads, else the position is a no-call. Returns true (no
// filtering) only when AD is missing/uninformative.
bool passesConsensusGate(int calledIdx, const std::vector<int>& ad, int minDepth);
// Same gate applied to a raw VCF sample field ("GT:PL:AD").
bool passesConsensusGate(const std::string& sampleField, int minDepth);
Expand Down
22 changes: 8 additions & 14 deletions src/index_single_mode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,8 @@ std::vector<panmapUtils::NewSyncmerRange> index_single_mode::IndexBuilder::compu
localRangeCoordToGlobalScalarCoords.push_back(curCoordScalar);
localRangeCoordToBlockId.push_back(curCoord.primaryBlockId);
} else if (refOnSyncmers.contains(curCoordScalar)) {
// Only delete seeds inside the genome extent. Flank regions (before
// firstNonGapScalar or after lastNonGapScalar) are missing data, not true
// gaps; their seeds stay inherited from the parent.
// Only delete seeds inside the genome extent; flank regions are missing
// data, not true gaps, so their seeds stay inherited from the parent.
if (std::cmp_greater_equal(curCoordScalar, firstNonGapScalar) &&
std::cmp_less_equal(curCoordScalar, lastNonGapScalar)) {
blockOnSyncmers[curCoord.primaryBlockId].erase(curCoordScalar);
Expand All @@ -368,9 +367,8 @@ std::vector<panmapUtils::NewSyncmerRange> index_single_mode::IndexBuilder::compu
recomputeBlock = false;
recomputeInProgress = false;
if (curCoordGapMapIt != gapMap.end()) {
// At begin() there is no previous run, so the lower bound
// (prev->second < curCoordScalar) is vacuously satisfied;
// guard the std::prev to avoid dereferencing before begin().
// At begin() there is no previous run (lower bound vacuously
// satisfied); guard std::prev to avoid dereferencing before begin().
while (curCoordGapMapIt != gapMap.end() &&
!((curCoordGapMapIt == gapMap.begin() ||
std::cmp_less(std::prev(curCoordGapMapIt)->second, curCoordScalar)) &&
Expand Down Expand Up @@ -1386,11 +1384,8 @@ void index_single_mode::IndexBuilder::computeSubstitutionSpectrum() {
uint32_t blockId = blockMut.primaryBlockId;
bool oldExists = blockSequences.blockExists[blockId];
blockUndoRecord.emplace_back(blockId, oldExists);
// Match the canonical block-existence rule (panmap_utils.cpp): an
// insertion makes the block exist (even when also inverted); a
// non-inversion deletion removes it; an inversion of an existing
// block leaves existence unchanged. blockMutInfo=false marks both
// deletions and inversions, so it can't be assigned directly.
// Canonical block-existence rule (panmap_utils.cpp): insertion sets exists, non-inversion
// deletion clears it, inversion leaves it unchanged; blockMutInfo=false marks del+inv both.
if (blockMut.blockMutInfo) {
blockSequences.blockExists[blockId] = true;
} else if (!blockMut.inversion) {
Expand Down Expand Up @@ -2367,9 +2362,8 @@ void index_single_mode::IndexBuilder::buildIndexParallel(int numThreads) {

totalClonesCreated_.fetch_add(1, std::memory_order_relaxed);

// Walk from root to the range's first node, then DFS through the range.
// Nodes 0..startDfs-1 are ancestors or earlier siblings that must be walked.
// Compute nodeChanges only for nodes in [startDfs, endDfs).
// Walk from root to the range's first node, then DFS through the range. Compute
// nodeChanges only for nodes in [startDfs, endDfs); earlier nodes are just walked.

std::function<void(panmanUtils::Node*, uint64_t)> processDfsRange = [&](panmanUtils::Node* node,
uint64_t dfsIdx) {
Expand Down
17 changes: 6 additions & 11 deletions src/index_single_mode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,16 @@

namespace index_single_mode {

// A small uncompressed header prepended to the .idx so cache-validation can read the
// seeding params without decompressing the (up to GB-scale) payload -- previously the
// validation step fully decompressed the index just to read six fields, then the
// placement step decompressed it a second time.
// Uncompressed header prepended to the .idx so cache-validation can read the seeding
// params without decompressing the (up to GB-scale) payload.
constexpr uint32_t kIndexMagic = 0x31494D50u; // "PMI1" little-endian
constexpr uint32_t kIndexHeaderVersion = 1;
constexpr size_t kIndexHeaderSize = 32;
struct IndexParamsHeader {
int32_t k = 0, s = 0, t = 0, l = 0;
bool hpc = false, open = false;
// When set, the payload after the header is raw (unframed) capnp bytes rather than
// ZSTD frames, so load can mmap it and hand the bytes straight to capnp (zero-copy,
// no decompression pass). Bigger on disk, faster to load.
// When set, payload is raw (unframed) capnp bytes, not ZSTD frames, so load can mmap
// and hand them straight to capnp (zero-copy). Bigger on disk, faster to load.
bool uncompressed = false;
};
std::array<uint8_t, kIndexHeaderSize> encodeIndexHeader(const IndexParamsHeader& p);
Expand Down Expand Up @@ -69,10 +66,8 @@ struct BuildState {
std::map<uint64_t, uint64_t> gapMap;
std::unordered_set<uint64_t> invertedBlocks;

// first/last non-gap scalar positions. Seeds in flank regions (before
// firstNonGapScalar or after lastNonGapScalar) are missing data, not true gaps,
// so must not be deleted when they become gaps. Permissive by default; only
// restricted when extentGuard is on.
// first/last non-gap scalar positions. Seeds in flank regions are missing data, not
// true gaps, so must not be deleted when they become gaps; permissive unless extentGuard is on.
uint64_t firstNonGapScalar = 0;
uint64_t lastNonGapScalar = UINT64_MAX;

Expand Down
31 changes: 5 additions & 26 deletions src/logging.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ inline const char* cross() {
}
} // namespace box

// Spinner frames (Braille pattern, 10 frames at ~80ms = ~12fps)
inline const char* spinner_frame(int i) {
static const char* frames[] = {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"};
static const char* plain_frames[] = {"|", "/", "-", "\\", "|", "/", "-", "\\", "|", "/"};
Expand Down Expand Up @@ -165,8 +164,6 @@ inline void debug(const std::string& msg) {
std::cerr << style::dim() << msg << style::reset() << "\n";
}

// Buffered single-line dim stream for routine log lines. detail() visible unless
// --quiet; trace() only under --verbose.
class LineBuf {
public:
explicit LineBuf(bool enabled) : enabled_(enabled) {}
Expand All @@ -181,7 +178,7 @@ class LineBuf {
return *this;
}

// ostream manipulators (std::endl / std::flush) flush the line immediately.
// std::endl / std::flush flush the line immediately.
LineBuf& operator<<(std::ostream& (*)(std::ostream&)) {
emit();
return *this;
Expand Down Expand Up @@ -211,18 +208,13 @@ inline LineBuf trace() {
return LineBuf(config().verbose && !config().quiet);
}

// One-line tool+version banner.
inline void banner(const std::string& version, const std::string& /*subtitle*/ = "") {
if (config().quiet) return;
std::cerr << style::bold() << "panmap" << style::reset() << style::dim() << " " << version << style::reset()
<< "\n\n";
}

// Stage line columns:
// " ICN LABEL. SUBJECT................. STAT......... TIME"
// Subject/stat are left-padded to a min width so columns align across stages;
// time is right-padded so right edges line up. Input strings must be plain text
// (no ANSI codes) or the width math breaks; apply styling via the helpers below.
// Stage-line inputs must be plain text (no ANSI) or the column width math breaks.
constexpr int kLabelWidth = 6;
constexpr int kSubjectWidth = 22;
constexpr int kStatWidth = 14;
Expand All @@ -242,8 +234,7 @@ inline std::string pad_left(const std::string& s, int w) {
return std::string(w - s.size(), ' ') + s;
}

// Truncate to w columns, keeping the tail (informative end of a path) behind a
// leading ellipsis. Assumes ASCII content.
// Keeps the tail behind a leading ellipsis. Assumes ASCII content.
inline std::string truncate_tail(const std::string& s, int w) {
if (w <= 0) return "";
if ((int)s.size() <= w) return s;
Expand All @@ -270,9 +261,6 @@ inline void write_status_line(const std::string& icon,
const std::string& stat,
int64_t ms) {
if (config().quiet) return;
// Subject column flexes to terminal width so stat/time columns stay aligned
// across rows; long subjects (paths) are tail-truncated. Non-TTY (piped/log)
// keeps the full subject and the original min width.
int subjectW;
if (config().isTTY) {
int iconW = config().plain ? static_cast<int>(icon.size()) : 1;
Expand Down Expand Up @@ -312,7 +300,6 @@ inline void done(const std::string& what, int64_t ms) {
<< style::reset() << "\n";
}

// Trailing blank line before the shell prompt.
inline void summary(int64_t /*total_ms*/) {
if (config().quiet) return;
std::cerr << "\n";
Expand All @@ -335,7 +322,6 @@ inline void step(const std::string& msg) {
std::cerr << style::dim() << " " << msg << style::reset() << "\n";
}

// Indeterminate progress message (one line, overwritten in place).
inline void progress(const std::string& msg) {
if (config().quiet || !config().isTTY || config().plain) return;
std::cerr << "\r\033[K" << style::dim() << " " << msg << style::reset() << std::flush;
Expand All @@ -345,9 +331,6 @@ inline void progress_clear() {
if (config().isTTY && !config().plain) std::cerr << "\r\033[K" << std::flush;
}

// Progress bar. In-place line:
// <spinner> <label> ━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 42% · 1.2s
// clear() leaves the line empty so the caller can emit a done() line.
class ProgressBar {
public:
ProgressBar(std::string label, uint64_t total) : label_(std::move(label)), total_(total) {
Expand Down Expand Up @@ -399,11 +382,10 @@ class ProgressBar {
std::string pct = fmt::format("{:>3}%", static_cast<int>(frac * 100));
std::string ela = format_duration(elapsed);

// Layout: " <spin> <label-padded> <bar> <pct> · <ela>"
std::string label_padded = label_;
if ((int)label_padded.size() < kLabelWidth) label_padded.append(kLabelWidth - label_padded.size(), ' ');

// Reserved visible chars (no ANSI): " X " + label + " " + bar + " " + "NNN%" + " · " + ela
// Reserved visible (no ANSI) cols: " X " + label + " " + bar + " " + "NNN%" + " · " + ela
int reserved = 2 + 1 + 1 + (int)label_padded.size() + 1 + 2 + 4 + 4 + (int)ela.size();
int width = term_width();
int bar_w = width - reserved - 2;
Expand Down Expand Up @@ -457,10 +439,7 @@ inline bool check_interrupted(bool print_message = true) {
return false;
}

// Async-signal-safe handler. Exits immediately on first Ctrl-C/SIGTERM rather than
// polling a flag: long stages (8M-node index, EM rounds) never poll, so a polite
// path looks stuck. Outputs are written all-at-once at stage end, so an interrupt
// mid-stage leaves no partial artifact.
// Async-signal-safe: _Exit immediately rather than poll a flag (long stages never poll).
inline void handler(int signum) {
(void)signum;
static const char msg[] = "\ninterrupted\n";
Expand Down
Loading
Loading