From 811cacdf289152c5dc610080ead970ecb88d9bbf Mon Sep 17 00:00:00 2001 From: Andrew D Smith Date: Mon, 17 Nov 2025 11:17:23 -0800 Subject: [PATCH 1/7] src/amrfinder/amrfinder.cpp: fixing the block bounds which was causing a bug when more than one thread was used --- src/amrfinder/amrfinder.cpp | 257 ++++++++++++++++++------------------ 1 file changed, 132 insertions(+), 125 deletions(-) diff --git a/src/amrfinder/amrfinder.cpp b/src/amrfinder/amrfinder.cpp index 9b33115c..36f9b012 100644 --- a/src/amrfinder/amrfinder.cpp +++ b/src/amrfinder/amrfinder.cpp @@ -18,7 +18,10 @@ */ #include "EpireadStats.hpp" -#include "GenomicRegion.hpp" + +#include "Interval.hpp" +#include "Interval6.hpp" + #include "OptionParser.hpp" #include "smithlab_os.hpp" #include "smithlab_utils.hpp" @@ -26,8 +29,11 @@ #include #include +#include #include +#include #include +#include #include #include #include @@ -35,13 +41,11 @@ #include struct amr_summary { - amr_summary(const std::vector &amrs) { - amr_count = std::size(amrs); - amr_total_size = - accumulate(std::cbegin(amrs), std::cend(amrs), 0ul, - [](const std::size_t t, const GenomicRegion &p) { - return t + p.get_width(); - }); + amr_summary(const std::vector &amrs) { + amr_count = size(amrs); + amr_total_size = accumulate( + std::cbegin(amrs), std::cend(amrs), 0ul, + [](const std::size_t t, const Interval6 &p) { return t + size(p); }); amr_mean_size = static_cast(amr_total_size) / std::max(amr_count, static_cast(1)); } @@ -99,13 +103,13 @@ using epi_r = small_epiread; /* merges amrs within some pre-defined distance */ static void -merge_amrs(const std::size_t gap_limit, std::vector &amrs) { +merge_amrs(const std::size_t gap_limit, std::vector &amrs) { auto j = std::begin(amrs); for (const auto &a : amrs) // check distance between two amrs is greater than gap limit - if (j->same_chrom(a) && j->get_end() + gap_limit >= a.get_start()) { - j->set_end(a.get_end()); - j->set_score(std::min(a.get_score(), j->get_score())); + if (j->chrom == a.chrom && j->stop + gap_limit >= a.start) { + j->stop = a.stop; + j->score = std::min(a.score, j->score); } else *(++j) = a; @@ -113,13 +117,7 @@ merge_amrs(const std::size_t gap_limit, std::vector &amrs) { amrs.erase(++j, std::cend(amrs)); } -//////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////////////// -///// -///// CODE FOR RANDOMIZING THE READS TO GET EXPECTED NUMBER OF -///// IDENTIFIED AMRS -///// +// code for randomizing the reads to get expected number of identified amrs // static void // set_read_states(std::vector > &state_counts, @@ -158,10 +156,10 @@ merge_amrs(const std::size_t gap_limit, std::vector &amrs) { // } // Code for converting between cpg and base pair coordinates below -static inline std::vector -collect_cpgs(const std::string &s) { +[[nodiscard]] static inline auto +collect_cpgs(const std::string &s) -> std::vector { std::vector cpgs; - for (std::uint32_t i = 0; i < std::size(s) - 1; ++i) + for (std::uint32_t i = 0; i + 1 < size(s); ++i) if (s[i] == 'C' && s[i + 1] == 'G') cpgs.push_back(i); return cpgs; @@ -169,48 +167,51 @@ collect_cpgs(const std::string &s) { template static inline bool -convert_coordinates(const std::vector &cpgs, GenomicRegion ®ion) { - if (std::size(cpgs) <= region.get_end()) +convert_coordinates(const std::vector &cpgs, Interval6 ®ion) { + if (size(cpgs) <= region.stop) return false; - region.set_start(cpgs[region.get_start()]); - region.set_end(cpgs[region.get_end()]); + region.start = cpgs[region.start]; + region.stop = cpgs[region.stop]; return true; } static std::vector> -get_chrom_partition(const std::vector &r) { +get_chrom_partition(const std::vector &r) { if (r.empty()) return {}; std::vector> parts; - std::string prev_chrom{r.front().get_chrom()}; + std::string prev_chrom{r.front().chrom}; std::uint32_t prev_idx = 0u; - for (auto i = 0u; i < std::size(r); ++i) - if (r[i].get_chrom() != prev_chrom) { + for (auto i = 0u; i < size(r); ++i) + if (r[i].chrom != prev_chrom) { parts.emplace_back(prev_idx, i); prev_idx = i; - prev_chrom = r[i].get_chrom(); + prev_chrom = r[i].chrom; } - parts.emplace_back(prev_idx, std::size(r)); + parts.emplace_back(prev_idx, size(r)); return parts; } [[nodiscard]] static bool convert_coordinates(const std::size_t n_threads, const std::string &genome_file, - std::vector &amrs) { - std::vector c_name, c_seq; - read_fasta_file_short_names(genome_file, c_name, c_seq); - const std::size_t n_chroms = std::size(c_seq); - - for (auto &s : c_seq) - for (auto &c : s) - c = std::toupper(c); + std::vector &amrs) { std::unordered_map chrom_lookup; - for (auto i = 0u; i < n_chroms; ++i) - chrom_lookup.emplace(std::move(c_name[i]), std::move(c_seq[i])); + { + std::vector c_name, c_seq; + read_fasta_file_short_names(genome_file, c_name, c_seq); + const std::size_t n_chroms = size(c_seq); + + for (auto &s : c_seq) + for (auto &c : s) + c = std::toupper(c); + + for (auto i = 0u; i < n_chroms; ++i) + chrom_lookup.emplace(c_name[i], c_seq[i]); + } const auto chrom_parts = get_chrom_partition(amrs); - const auto n_parts = std::size(chrom_parts); + const auto n_parts = size(chrom_parts); const auto parts_beg = std::cbegin(chrom_parts); const std::uint32_t n_per = (n_parts + n_threads - 1) / n_threads; @@ -222,10 +223,10 @@ convert_coordinates(const std::size_t n_threads, const std::string &genome_file, const auto p_end = parts_beg + std::min((i + 1) * n_per, n_parts); threads.emplace_back([&, p_beg, p_end] { for (auto p = p_beg; p != p_end; ++p) { - const std::string chrom_name = amrs[p->first].get_chrom(); + const std::string chrom_name = amrs[p->first].chrom; const auto c_itr = chrom_lookup.find(chrom_name); if (c_itr == std::cend(chrom_lookup)) - conv_failure++; + ++conv_failure; else { std::vector cpgs = collect_cpgs(c_itr->second); for (auto j = p->first; j < p->second; ++j) @@ -252,7 +253,7 @@ clip_read(const std::size_t start_pos, const std::size_t end_pos, epi_r r) { return r; } -static std::vector +[[nodiscard]] static std::vector get_current_epireads(const std::vector &epireads, const std::size_t max_len, const std::size_t cpg_window, const std::size_t start_pos, std::size_t &read_id) { @@ -260,14 +261,11 @@ get_current_epireads(const std::vector &epireads, // [](const epi_r &a, const epi_r &b) { // return a.pos < b.pos; // })); - const auto n_epi = std::size(epireads); - - while (read_id < std::size(epireads) && - epireads[read_id].pos + max_len <= start_pos) + const auto n_epi = size(epireads); + while (read_id < n_epi && epireads[read_id].pos + max_len <= start_pos) ++read_id; const auto end_pos = start_pos + cpg_window; - std::vector current_epireads; for (auto i = read_id; i < n_epi && epireads[i].pos < end_pos; ++i) if (epireads[i].end() > start_pos) @@ -287,9 +285,9 @@ total_states(const std::vector &epireads) { static inline void add_amr(const std::string &chrom_name, const std::size_t start_cpg, const std::size_t cpg_window, const std::vector &reads, - const double score, std::vector &amrs) { + const double score, std::vector &amrs) { const auto end_cpg = start_cpg + cpg_window - 1; - const auto rds = std::to_string(std::size(reads)); + const auto rds = std::to_string(size(reads)); amrs.emplace_back(chrom_name, start_cpg, end_cpg, rds, score, '+'); } @@ -302,18 +300,20 @@ get_n_cpgs(const std::vector &reads) { } template -static inline std::vector> -get_block_bounds(const T start_pos, const T end_pos, const T block_size) { - if (block_size == 0) - return {{start_pos, end_pos}}; - std::vector> blocks; - auto block_start = start_pos; - while (block_start < end_pos) { - const auto block_end = std::min({block_start + block_size, end_pos}); - blocks.emplace_back(block_start, block_end); +[[nodiscard]] static inline auto +get_block_bounds(const T n_elements, const T n_chunks) + -> std::vector> { + const auto q = n_elements / n_chunks; + const auto r = n_elements - q * n_chunks; + std::vector> chunks(n_chunks); + T block_start{}; + for (std::size_t i = 0; i < n_chunks; ++i) { + const auto sz = (i < r) ? q + 1 : q; + const auto block_end = block_start + sz; + chunks[i] = {block_start, block_end}; block_start = block_end; } - return blocks; + return chunks; } static std::size_t @@ -321,39 +321,50 @@ process_chrom(const bool verbose, const std::uint32_t n_threads, const std::size_t min_obs_per_cpg, const std::size_t window_size, const EpireadStats &epistat, const std::string &chrom_name, const std::vector &epireads, - std::vector &amrs) { - constexpr auto blocks_per_thread = 8u; + std::vector &amrs) { + constexpr auto blocks_per_thread = 1u; auto max_epiread_len = 0u; for (auto &e : epireads) max_epiread_len = std::max(max_epiread_len, e.length()); const std::size_t min_obs_per_window = window_size * min_obs_per_cpg; - const std::size_t n_cpgs = get_n_cpgs(epireads); + const std::uint32_t n_cpgs = get_n_cpgs(epireads); if (verbose) - std::cerr << "processing " << chrom_name << " " - << "[reads: " << std::size(epireads) << "] " - << "[cpgs: " << n_cpgs << "]\n"; - - const auto n_blocks = n_threads * blocks_per_thread; + std::println(std::cerr, "processing {} [reads: {}][cpgs: {}]", chrom_name, + size(epireads), n_cpgs); const std::uint32_t lim = n_cpgs >= window_size ? n_cpgs - window_size + 1 : 0; - const auto blocks = get_block_bounds(0u, lim, lim / n_blocks); + if (lim == 0) + return 0; + + // ADS: need to do this by windows + const auto n_blocks = std::min(lim, n_threads * blocks_per_thread); + const auto blocks = get_block_bounds(lim, n_blocks); + if (!(n_blocks == size(blocks))) { + std::cerr << "n_blocks=" << n_blocks << '\t' + << "std::size(blocks)=" << size(blocks) << '\t' << "lim=" << lim + << '\t' << "lim/n_blocks=" << lim / n_blocks << std::endl; + exit(0); + } const auto blocks_beg = std::cbegin(blocks); const std::uint32_t n_per = (n_blocks + n_threads - 1) / n_threads; - const auto n_epireads = std::size(epireads); + const auto n_epireads = size(epireads); std::atomic_ulong windows_tested = 0; - std::vector> all_amrs(n_threads); + std::vector> all_amrs(n_threads); std::vector threads; - for (auto i = 0u; i < n_threads; ++i) { - const auto b_beg = blocks_beg + i * n_per; + for (auto i = 0u; i < std::min(n_threads, n_blocks); ++i) { + const auto b_beg = + blocks_beg + std::min(i * n_per, n_blocks); // i * n_per; const auto b_end = blocks_beg + std::min((i + 1) * n_per, n_blocks); + if (b_beg == b_end) + break; threads.emplace_back([&, i, b_beg, b_end] { - std::vector curr_amrs; + std::vector curr_amrs; std::size_t windows_tested_thread = 0; for (auto b = b_beg; b != b_end; ++b) { std::size_t start_idx = 0; @@ -372,7 +383,7 @@ process_chrom(const bool verbose, const std::uint32_t n_threads, } } } - all_amrs[i] = std::move(curr_amrs); + all_amrs[i] = curr_amrs; windows_tested += windows_tested_thread; }); } @@ -381,7 +392,7 @@ process_chrom(const bool verbose, const std::uint32_t n_threads, auto total_amrs = 0u; for (const auto &a : all_amrs) - total_amrs += std::size(a); + total_amrs += size(a); amrs.reserve(total_amrs); for (auto &v : all_amrs) @@ -393,9 +404,9 @@ process_chrom(const bool verbose, const std::uint32_t n_threads, struct rename_amr { void - operator()(GenomicRegion &r) { + operator()(Interval6 &r) { static constexpr auto label = "AMR"; - r.set_name(label + std::to_string(idx++) + ":" + r.get_name()); + r.name = label + std::to_string(idx++) + ":" + r.name; } std::uint32_t idx{}; }; @@ -499,7 +510,7 @@ main_amrfinder(int argc, char *argv[]) { if (n_threads > 1 && in.is_bgzf()) tp.set_io(in); - std::vector amrs; + std::vector amrs; // windows_tested is the number of sliding windows in the // methylome that were tested for a signal of significant @@ -516,8 +527,8 @@ main_amrfinder(int argc, char *argv[]) { epistat, prev_chrom, epireads, amrs); epireads.clear(); } - swap(prev_chrom, er.chr); epireads.emplace_back(er.pos, er.seq); + swap(prev_chrom, er.chr); } if (!epireads.empty()) windows_tested += @@ -531,19 +542,18 @@ main_amrfinder(int argc, char *argv[]) { std::for_each(std::begin(amrs), std::end(amrs), rename_amr()); if (verbose) - std::cerr << "========= POST PROCESSING =========\n"; + std::println(std::cerr, "========= POST PROCESSING ========="); // windows_accepted is the number of sliding windows in the // methylome that were found to have a significant signal of // allele-specific methylation - const auto windows_accepted = std::size(amrs); + const auto windows_accepted = size(amrs); if (!amrs.empty()) { /* - ADS: there are several "steps" below that are done - independently, but for which the order matters. It is not - clear to me that this is the right order, after observing the - behavior of the program on some newer much larger data - sets. The steps: + ADS: there are several "steps" below that are done independently, but + for which the order matters. It is not clear to me that this is the + right order, after observing the behavior of the program on some newer + much larger data sets. The steps: (1) Get the p-values (2) Get the fdr cutoff @@ -554,16 +564,15 @@ main_amrfinder(int argc, char *argv[]) { (7) remove the AMRs based on a score (p-value or fdr) (8) eliminate what remains by size using half the "gap limit" - The score in the above is taken as the extreme from step (4), - which interacts poorly with subsequent steps. Also, the "half - gap limit" for eliminating AMRs at the end is not justified - anywhere. + The score in the above is taken as the extreme from step (4), which + interacts poorly with subsequent steps. Also, the "half gap limit" for + eliminating AMRs at the end is not justified anywhere. */ // get all the pvals... (but they might be BIC scores) - std::vector pvals(std::size(amrs)); + std::vector pvals(size(amrs)); transform(std::cbegin(amrs), std::cend(amrs), std::begin(pvals), - [](const GenomicRegion &r) { return r.get_score(); }); + [](const Interval6 &r) { return r.score; }); // if the pvalues are not BIC scores, get an FDR cutoff // corresponding to the given critical value @@ -572,60 +581,58 @@ main_amrfinder(int argc, char *argv[]) { ? 0.0 : smithlab::get_fdr_cutoff(windows_tested, pvals, critical_value); - // if we are not using BIC, and if corrected p-values are - // requested, then adjust the p-values + // if we are not using BIC, and if corrected p-values are requested, + // then adjust the p-values if (!use_bic && apply_correction) { smithlab::correct_pvals(windows_tested, pvals); - for (auto i = 0u; i < std::size(pvals); ++i) - amrs[i].set_score(pvals[i]); + for (auto i = 0u; i < size(pvals); ++i) + amrs[i].score = pvals[i]; } - // ADS: not sure it's a good idea in this next collapse function - // to keep the lowest among all p-values for merged regions + // ADS: not sure it's a good idea in this next collapse function to keep + // the lowest among all p-values for merged regions merge_amrs(1, amrs); - // collapsed_amrs is the number of intervals of consecutive CpG - // sites that are found to reside in a window among those - // accepted as significant - const auto n_collapsed_amrs = std::size(amrs); + // collapsed_amrs is the number of intervals of consecutive CpG sites + // that are found to reside in a window among those accepted as + // significant + const auto n_collapsed_amrs = size(amrs); if (!convert_coordinates(n_threads, genome_file, amrs)) { std::cerr << "failed converting coordinates\n"; return EXIT_FAILURE; } - // ADS: merge AMRs if they are sufficiently close; the distance - // should not be too large, and a distance of 1000 bp is likely - // too large. + // ADS: merge AMRs if they are sufficiently close; the distance should + // not be too large, and a distance of 1000 bp is likely too large. merge_amrs(gap_limit, amrs); - // merged_amrs are the number of intervals that result from - // merging any collapsed amrs that have a distance of less than - // gap_limit/2 from each other - const auto n_merged_amrs = std::size(amrs); + // merged_amrs are the number of intervals that result from merging any + // collapsed amrs that have a distance of less than gap_limit/2 from + // each other + const auto n_merged_amrs = size(amrs); - // if BIC was not requested, then eliminate AMRs based on either - // the p-value cutoff, or with the FDR-based cutoff, if it was - // requested. + // if BIC was not requested, then eliminate AMRs based on either the + // p-value cutoff, or with the FDR-based cutoff, if it was requested. if (!use_bic) { const auto cutoff = (apply_correction || !use_fdr) ? critical_value : fdr_cutoff; amrs.erase(std::remove_if(std::begin(amrs), std::end(amrs), - [cutoff](const GenomicRegion &r) { - return r.get_score() >= cutoff; + [cutoff](const Interval6 &r) { + return r.score >= cutoff; }), std::cend(amrs)); } - const auto amrs_passing_fdr = std::size(amrs); + const auto amrs_passing_fdr = size(amrs); - // ADS: eliminating AMRs based on their size makes sense, but - // not if that size is tied to the gap between AMRs we would - // merge. There is no symmetry for these. + // ADS: eliminating AMRs based on their size makes sense, but not if + // that size is tied to the gap between AMRs we would merge. There is no + // symmetry for these. const auto min_size = gap_limit / 2; amrs.erase( std::remove_if(std::begin(amrs), std::end(amrs), - [&](const auto &r) { return r.get_width() < min_size; }), + [&](const auto &r) { return size(r) < min_size; }), std::cend(amrs)); if (verbose) { @@ -641,8 +648,8 @@ main_amrfinder(int argc, char *argv[]) { std::ofstream out(outfile); if (!out) std::runtime_error("failed to open output file: " + outfile); - std::copy(std::cbegin(amrs), std::cend(amrs), - std::ostream_iterator(out, "\n")); + for (const auto &a : amrs) + std::println(out, "{}", a); } if (!summary_file.empty()) { std::ofstream summary_out(summary_file); From 4956d33c8871526eb74c7b9f2eb74807b2f93083 Mon Sep 17 00:00:00 2001 From: Andrew D Smith Date: Tue, 18 Nov 2025 12:24:05 -0800 Subject: [PATCH 2/7] src/common/Interval6.hcpp and src/common/Interval.hcpp: adding these files to replace GenomicRegion from smithlab_cpp soon --- src/common/Interval.cpp | 77 ++++++++++++++++++++++++++++ src/common/Interval.hpp | 66 ++++++++++++++++++++++++ src/common/Interval6.cpp | 105 +++++++++++++++++++++++++++++++++++++++ src/common/Interval6.hpp | 71 ++++++++++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 src/common/Interval.cpp create mode 100644 src/common/Interval.hpp create mode 100644 src/common/Interval6.cpp create mode 100644 src/common/Interval6.hpp diff --git a/src/common/Interval.cpp b/src/common/Interval.cpp new file mode 100644 index 00000000..3b0b6915 --- /dev/null +++ b/src/common/Interval.cpp @@ -0,0 +1,77 @@ +/* Copyright (C) 2025 Andrew D Smith + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along + * with this software; if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "Interval.hpp" + +#include +#include +#include +#include +#include +#include + +auto +Interval::initialize(const char *c, const char *c_end) -> bool { + constexpr auto is_sep = [](const char x) { return x == ' ' || x == '\t'; }; + constexpr auto not_sep = [](const char x) { return x != ' ' && x != '\t'; }; + + bool failed = false; + + // NOLINTBEGIN(*-pointer-arithmetic) + auto field_s = c; + auto field_e = std::find_if(field_s + 1, c_end, is_sep); + if (field_e == c_end) + failed = true; + + // chrom + { + const std::uint32_t d = std::distance(field_s, field_e); + chrom = std::string{field_s, d}; + } + + // start + field_s = std::find_if(field_e + 1, c_end, not_sep); + field_e = std::find_if(field_s + 1, c_end, is_sep); + failed = failed || (field_e == c_end); + { + const auto [ptr, ec] = std::from_chars(field_s, field_e, start); + failed = failed || ec != std::errc{}; + } + + // stop + field_s = std::find_if(field_e + 1, c_end, not_sep); + field_e = std::find_if(field_s + 1, c_end, is_sep); + failed = failed || (field_e == c_end); + { + const auto [ptr, ec] = std::from_chars(field_s, field_e, stop); + failed = failed || ec != std::errc{}; + } + + return !failed; +} + +[[nodiscard]] auto +read_intervals(const std::string &intervals_file) -> std::vector { + std::ifstream in(intervals_file); + if (!in) + throw std::runtime_error("failed to open file: " + intervals_file); + std::string line; + std::vector intervals; + while (getline(in, line)) + intervals.emplace_back(line); + return intervals; +} diff --git a/src/common/Interval.hpp b/src/common/Interval.hpp new file mode 100644 index 00000000..ff685550 --- /dev/null +++ b/src/common/Interval.hpp @@ -0,0 +1,66 @@ +/* Copyright (C) 2025 Andrew D Smith + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along + * with this software; if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef INTERVAL_HPP_ +#define INTERVAL_HPP_ + +#include +#include +#include // std::size +#include +#include +#include + +struct Interval { + std::string chrom; + std::uint32_t start{}; + std::uint32_t stop{}; + + Interval() = default; + Interval(const std::string &chrom, const std::uint32_t start, + const std::uint32_t stop) : + chrom{chrom}, + start{start}, stop{stop} {} + + explicit Interval(const std::string &line) { + if (!initialize(line.data(), line.data() + std::size(line))) + throw std::runtime_error("bad interval line: " + line); + } + auto + initialize(const char *, const char *) -> bool; + auto + operator<=>(const Interval &) const = default; +}; + +template <> struct std::formatter : std::formatter { + auto + format(const Interval &i, format_context &ctx) const { + static constexpr auto fmt = "{}\t{}\t{}"; + return std::formatter::format( + std::format(fmt, i.chrom, i.start, i.stop), ctx); + } +}; + +[[nodiscard]] inline auto +size(const Interval &x) { + return x.stop > x.start ? x.stop - x.start : 0ul; +} + +[[nodiscard]] auto +read_intervals(const std::string &intervals_file) -> std::vector; + +#endif // INTERVAL_HPP_ diff --git a/src/common/Interval6.cpp b/src/common/Interval6.cpp new file mode 100644 index 00000000..2a4fa1de --- /dev/null +++ b/src/common/Interval6.cpp @@ -0,0 +1,105 @@ +/* Copyright (C) 2025 Andrew D Smith + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along + * with this software; if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "Interval6.hpp" + +#include +#include +#include +#include +#include +#include +#include + +auto +Interval6::initialize(const char *c, const char *c_end) -> bool { + constexpr auto is_sep = [](const char x) { return x == ' ' || x == '\t'; }; + constexpr auto not_sep = [](const char x) { return x != ' ' && x != '\t'; }; + + bool failed = false; + + // NOLINTBEGIN(*-pointer-arithmetic) + auto field_s = c; + auto field_e = std::find_if(field_s + 1, c_end, is_sep); + if (field_e == c_end) + failed = true; + + // chrom + { + const std::uint32_t d = std::distance(field_s, field_e); + chrom = std::string{field_s, d}; + } + + // start + field_s = std::find_if(field_e + 1, c_end, not_sep); + field_e = std::find_if(field_s + 1, c_end, is_sep); + failed = failed || (field_e == c_end); + { + const auto [ptr, ec] = std::from_chars(field_s, field_e, start); + failed = failed || (ptr == field_s); + } + + // stop + field_s = std::find_if(field_e + 1, c_end, not_sep); + field_e = std::find_if(field_s + 1, c_end, is_sep); + failed = failed || (field_e == c_end); + { + const auto [ptr, ec] = std::from_chars(field_s, field_e, stop); + failed = failed || (ptr == field_s); + } + + // name + field_s = std::find_if(field_e + 1, c_end, not_sep); + field_e = std::find_if(field_s + 1, c_end, is_sep); + failed = failed || (field_e == c_end); + name = std::string(field_s, std::distance(field_s, field_e)); + + // score + field_s = std::find_if(field_e + 1, c_end, not_sep); + field_e = std::find_if(field_s + 1, c_end, is_sep); + failed = failed || (field_e == c_end); + { +#ifdef __APPLE__ + const int ret = std::sscanf(field_s, "%lf", &score); + failed = failed || (ret < 1); +#else + const auto [ptr, ec] = std::from_chars(field_s, field_e, score); + failed = failed || ec != std::errc{}; +#endif + } + + // strand (no stop; just one char and maybe end of line) + field_s = std::find_if(field_e + 1, c_end, not_sep); + failed = failed || (field_s == c_end); + strand = *field_s; + failed = failed || (strand != '-' && strand != '+'); + // NOLINTEND(*-pointer-arithmetic) + + return !failed; +} + +[[nodiscard]] auto +read_intervals6(const std::string &intervals_file) -> std::vector { + std::ifstream in(intervals_file); + if (!in) + throw std::runtime_error("failed to open file: " + intervals_file); + std::string line; + std::vector intervals; + while (getline(in, line)) + intervals.emplace_back(line); + return intervals; +} diff --git a/src/common/Interval6.hpp b/src/common/Interval6.hpp new file mode 100644 index 00000000..5d78afbf --- /dev/null +++ b/src/common/Interval6.hpp @@ -0,0 +1,71 @@ +/* Copyright (C) 2025 Andrew D Smith + * + * This is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along + * with this software; if not, write to the Free Software Foundation, Inc., 51 + * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef INTERVAL6_HPP_ +#define INTERVAL6_HPP_ + +#include +#include +#include // std::size +#include +#include +#include + +struct Interval6 { + std::string chrom; + std::uint32_t start{}; + std::uint32_t stop{}; + std::string name; + double score{}; + char strand{}; + + Interval6() = default; + Interval6(const std::string &chrom, const std::uint32_t start, + const std::uint32_t stop, const std::string &name, + const double score, const char strand) : + chrom{chrom}, + start{start}, stop{stop}, name{name}, score{score}, strand{strand} {} + + explicit Interval6(const std::string &line) { + if (!initialize(line.data(), line.data() + std::size(line))) + throw std::runtime_error("bad interval6 line: " + line); + } + auto + initialize(const char *, const char *) -> bool; + auto + operator<=>(const Interval6 &) const = default; +}; + +template <> struct std::formatter : std::formatter { + auto + format(const Interval6 &i, format_context &ctx) const { + static constexpr auto fmt = "{}\t{}\t{}\t{}\t{:.6g}\t{}"; + return std::formatter::format( + std::format(fmt, i.chrom, i.start, i.stop, i.name, i.score, i.strand), + ctx); + } +}; + +[[nodiscard]] inline auto +size(const Interval6 &x) { + return x.stop > x.start ? x.stop - x.start : 0ul; +} + +[[nodiscard]] auto +read_intervals6(const std::string &intervals_file) -> std::vector; + +#endif // INTERVAL6_HPP_ From fd068df25a614c8099223a2dc39ba2fcc36a6a03 Mon Sep 17 00:00:00 2001 From: Andrew D Smith Date: Tue, 18 Nov 2025 12:48:47 -0800 Subject: [PATCH 3/7] src/common/Interval6.hpp and src/common/Interval.hpp: making these work with c++17 for now --- src/common/Interval.hpp | 34 +++++++++++++++++++++++++--------- src/common/Interval6.hpp | 37 +++++++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/common/Interval.hpp b/src/common/Interval.hpp index ff685550..ee1fa02f 100644 --- a/src/common/Interval.hpp +++ b/src/common/Interval.hpp @@ -19,7 +19,7 @@ #define INTERVAL_HPP_ #include -#include +// #include // ADS: needs c++20 #include // std::size #include #include @@ -42,19 +42,35 @@ struct Interval { } auto initialize(const char *, const char *) -> bool; - auto - operator<=>(const Interval &) const = default; -}; -template <> struct std::formatter : std::formatter { auto - format(const Interval &i, format_context &ctx) const { - static constexpr auto fmt = "{}\t{}\t{}"; - return std::formatter::format( - std::format(fmt, i.chrom, i.start, i.stop), ctx); + operator<(const Interval &rhs) const { + return (chrom < rhs.chrom || + (chrom == rhs.chrom && + (start < rhs.start || (start == rhs.start && stop < rhs.stop)))); } + + // auto + // operator<=>(const Interval &) const = default; }; +[[nodiscard]] inline auto +to_string(const Interval &x) -> std::string { + return x.chrom + "\t" + std::to_string(x.start) + "\t" + + std::to_string(x.stop); +} + +// ADS: need to bump to c++20 for this +// +// template <> struct std::formatter : std::formatter { +// auto +// format(const Interval &i, format_context &ctx) const { +// static constexpr auto fmt = "{}\t{}\t{}"; +// return std::formatter::format( +// std::format(fmt, i.chrom, i.start, i.stop), ctx); +// } +// }; + [[nodiscard]] inline auto size(const Interval &x) { return x.stop > x.start ? x.stop - x.start : 0ul; diff --git a/src/common/Interval6.hpp b/src/common/Interval6.hpp index 5d78afbf..3b84e213 100644 --- a/src/common/Interval6.hpp +++ b/src/common/Interval6.hpp @@ -19,7 +19,7 @@ #define INTERVAL6_HPP_ #include -#include +// #include // ADS: needs c++20 #include // std::size #include #include @@ -46,20 +46,37 @@ struct Interval6 { } auto initialize(const char *, const char *) -> bool; - auto - operator<=>(const Interval6 &) const = default; -}; -template <> struct std::formatter : std::formatter { auto - format(const Interval6 &i, format_context &ctx) const { - static constexpr auto fmt = "{}\t{}\t{}\t{}\t{:.6g}\t{}"; - return std::formatter::format( - std::format(fmt, i.chrom, i.start, i.stop, i.name, i.score, i.strand), - ctx); + operator<(const Interval6 &rhs) const { + return (chrom < rhs.chrom || + (chrom == rhs.chrom && + (start < rhs.start || (start == rhs.start && stop < rhs.stop)))); } + + // auto + // operator<=>(const Interval6 &) const = default; }; +[[nodiscard]] inline auto +to_string(const Interval6 &x) -> std::string { + return x.chrom + "\t" + std::to_string(x.start) + "\t" + + std::to_string(x.stop) + "\t" + x.name + "\t" + + std::to_string(x.score) + "\t" + std::string(1, x.strand); +} + +// ADS: need to bump to c++20 for this +// +// template <> struct std::formatter : std::formatter { +// auto +// format(const Interval6 &i, format_context &ctx) const { +// static constexpr auto fmt = "{}\t{}\t{}\t{}\t{:.6g}\t{}"; +// return std::formatter::format( +// std::format(fmt, i.chrom, i.start, i.stop, i.name, i.score, i.strand), +// ctx); +// } +// }; + [[nodiscard]] inline auto size(const Interval6 &x) { return x.stop > x.start ? x.stop - x.start : 0ul; From 69c56b38ad741e54f369899bd96b412163652915 Mon Sep 17 00:00:00 2001 From: Andrew D Smith Date: Tue, 18 Nov 2025 12:49:11 -0800 Subject: [PATCH 4/7] configure.ac: new style m4 macro for setting c++ version --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index ffbc8065..3d0954ee 100644 --- a/configure.ac +++ b/configure.ac @@ -30,7 +30,7 @@ AS_IF([test "x$CXXFLAGS_set" != "xset"], [ ]) AC_MSG_NOTICE([CXXFLAGS is $CXXFLAGS]) -AX_CXX_COMPILE_STDCXX_17([noext], [mandatory]) +AX_CXX_COMPILE_STDCXX(17, [noext], [mandatory]) AC_PROG_RANLIB dnl recursively configure abismal and smithlab_cpp From 2fdf04e7ad9f7129d5632ffe5e8dcbe8357671bd Mon Sep 17 00:00:00 2001 From: Andrew D Smith Date: Tue, 18 Nov 2025 12:49:29 -0800 Subject: [PATCH 5/7] m4/ax_cxx_compile_stdcxx.m4: updating this macro --- m4/ax_cxx_compile_stdcxx.m4 | 153 ++++++++++++++++++++++++++++++++---- 1 file changed, 136 insertions(+), 17 deletions(-) diff --git a/m4/ax_cxx_compile_stdcxx.m4 b/m4/ax_cxx_compile_stdcxx.m4 index 43087b2e..fe6ae17e 100644 --- a/m4/ax_cxx_compile_stdcxx.m4 +++ b/m4/ax_cxx_compile_stdcxx.m4 @@ -10,13 +10,13 @@ # # Check for baseline language coverage in the compiler for the specified # version of the C++ standard. If necessary, add switches to CXX and -# CXXCPP to enable support. VERSION may be '11' (for the C++11 standard) -# or '14' (for the C++14 standard). +# CXXCPP to enable support. VERSION may be '11', '14', '17', '20', or +# '23' for the respective C++ standard version. # # The second argument, if specified, indicates whether you insist on an # extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g. # -std=c++11). If neither is specified, you get whatever works, with -# preference for an extended mode. +# preference for no added switch, and then for an extended mode. # # The third argument, if specified 'mandatory' or if left unspecified, # indicates that baseline support for the specified C++ standard is @@ -35,13 +35,16 @@ # Copyright (c) 2015 Moritz Klammler # Copyright (c) 2016, 2018 Krzesimir Nowak # Copyright (c) 2019 Enji Cooper +# Copyright (c) 2020 Jason Merrill +# Copyright (c) 2021, 2024 Jörn Heusipp +# Copyright (c) 2015, 2022, 2023, 2024 Olly Betts # # Copying and distribution of this file, with or without modification, are # permitted in any medium without royalty provided the copyright notice # and this notice are preserved. This file is offered as-is, without any # warranty. -#serial 11 +#serial 25 dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro dnl (serial version number 13). @@ -50,6 +53,8 @@ AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"], [$1], [14], [ax_cxx_compile_alternatives="14 1y"], [$1], [17], [ax_cxx_compile_alternatives="17 1z"], + [$1], [20], [ax_cxx_compile_alternatives="20"], + [$1], [23], [ax_cxx_compile_alternatives="23"], [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl m4_if([$2], [], [], [$2], [ext], [], @@ -62,6 +67,16 @@ AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl AC_LANG_PUSH([C++])dnl ac_success=no + m4_if([$2], [], [dnl + AC_CACHE_CHECK(whether $CXX supports C++$1 features by default, + ax_cv_cxx_compile_cxx$1, + [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [ax_cv_cxx_compile_cxx$1=yes], + [ax_cv_cxx_compile_cxx$1=no])]) + if test x$ax_cv_cxx_compile_cxx$1 = xyes; then + ac_success=yes + fi]) + m4_if([$2], [noext], [], [dnl if test x$ac_success = xno; then for alternative in ${ax_cxx_compile_alternatives}; do @@ -91,9 +106,18 @@ AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl dnl HP's aCC needs +std=c++11 according to: dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf dnl Cray's crayCC needs "-h std=c++11" + dnl MSVC needs -std:c++NN for C++17 and later (default is C++14) for alternative in ${ax_cxx_compile_alternatives}; do - for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do - cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}" MSVC; do + if test x"$switch" = xMSVC; then + dnl AS_TR_SH maps both `:` and `=` to `_` so -std:c++17 would collide + dnl with -std=c++17. We suffix the cache variable name with _MSVC to + dnl avoid this. + switch=-std:c++${alternative} + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_${switch}_MSVC]) + else + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + fi AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, $cachevar, [ac_save_CXX="$CXX" @@ -137,23 +161,44 @@ AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl dnl Test body for checking C++11 support m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11], - _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11] ) - dnl Test body for checking C++14 support m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14], - _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 - _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14] ) +dnl Test body for checking C++17 support + m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17], - _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 - _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 - _AX_CXX_COMPILE_STDCXX_testbody_new_in_17 + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17] +) + +dnl Test body for checking C++20 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_20], + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_20] ) +dnl Test body for checking C++23 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_23], + [_AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_20 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_23] +) + + dnl Tests for new features in C++11 m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[ @@ -165,7 +210,21 @@ m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[ #error "This is not a C++ compiler" -#elif __cplusplus < 201103L +// MSVC always sets __cplusplus to 199711L in older versions; newer versions +// only set it correctly if /Zc:__cplusplus is specified as well as a +// /std:c++NN switch: +// +// https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ +// +// The value __cplusplus ought to have is available in _MSVC_LANG since +// Visual Studio 2015 Update 3: +// +// https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros +// +// This was also the first MSVC version to support C++14 so we can't use the +// value of either __cplusplus or _MSVC_LANG to quickly rule out MSVC having +// C++11 or C++14 support, but we can check _MSVC_LANG for C++17 and later. +#elif __cplusplus < 201103L && !defined _MSC_VER #error "This is not a C++11 compiler" @@ -456,7 +515,7 @@ m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[ #error "This is not a C++ compiler" -#elif __cplusplus < 201402L +#elif __cplusplus < 201402L && !defined _MSC_VER #error "This is not a C++14 compiler" @@ -580,7 +639,7 @@ m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[ #error "This is not a C++ compiler" -#elif __cplusplus < 201703L +#elif (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 201703L #error "This is not a C++17 compiler" @@ -946,6 +1005,66 @@ namespace cxx17 } // namespace cxx17 -#endif // __cplusplus < 201703L +#endif // (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 201703L + +]]) + + +dnl Tests for new features in C++20 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_20], [[ + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 202002L + +#error "This is not a C++20 compiler" + +#else + +#include + +namespace cxx20 +{ + +// As C++20 supports feature test macros in the standard, there is no +// immediate need to actually test for feature availability on the +// Autoconf side. + +} // namespace cxx20 + +#endif // (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 202002L + +]]) + + +dnl Tests for new features in C++23 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_23], [[ + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 202302L + +#error "This is not a C++23 compiler" + +#else + +#include + +namespace cxx23 +{ + +// As C++23 supports feature test macros in the standard, there is no +// immediate need to actually test for feature availability on the +// Autoconf side. + +} // namespace cxx23 + +#endif // (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 202302L ]]) From 419fd7a7194f7f59071c371be0a283ebce3c24ef Mon Sep 17 00:00:00 2001 From: Andrew D Smith Date: Tue, 18 Nov 2025 12:49:55 -0800 Subject: [PATCH 6/7] Makefile.am: adding Interval and Interval6 to replace GenomicRegion eventually --- Makefile.am | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile.am b/Makefile.am index bb85391d..f59f583f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -147,6 +147,8 @@ test_scripts/test_unxcounts.log: test_scripts/test_xcounts.log noinst_LIBRARIES = libdnmtools.a libdnmtools_a_SOURCES = \ + src/common/Interval.cpp \ + src/common/Interval6.cpp \ src/common/dnmtools_gaussinv.cpp \ src/common/bam_record_utils.cpp \ src/common/counts_header.cpp \ @@ -166,6 +168,8 @@ libdnmtools_a_SOURCES = \ src/common/dnmtools_utils.cpp libdnmtools_a_SOURCES += \ + src/common/Interval.hpp \ + src/common/Interval6.hpp \ src/bamxx/bamxx.hpp \ src/common/dnmtools_gaussinv.hpp \ src/common/bam_record_utils.hpp \ From 39a651e9720fccab685c7f25ae8020be4e557208 Mon Sep 17 00:00:00 2001 From: Andrew D Smith Date: Tue, 18 Nov 2025 12:50:43 -0800 Subject: [PATCH 7/7] src/amrfinder/amrfinder.cpp: making this work with c++17 for now --- src/amrfinder/amrfinder.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/amrfinder/amrfinder.cpp b/src/amrfinder/amrfinder.cpp index 36f9b012..49f4e6c3 100644 --- a/src/amrfinder/amrfinder.cpp +++ b/src/amrfinder/amrfinder.cpp @@ -33,7 +33,7 @@ #include #include #include -#include +// #include // ADS: needs c++20 #include #include #include @@ -50,9 +50,8 @@ struct amr_summary { std::max(amr_count, static_cast(1)); } - // amr_count is the number of identified AMRs, which are the merged - // AMRs that are found to be significant when tested as a single - // interval + // amr_count is the number of identified AMRs, which are the merged AMRs + // that are found to be significant when tested as a single interval std::size_t amr_count{}; // total_amr_size is the sum of the sizes of the identified AMRs std::size_t amr_total_size{}; @@ -331,8 +330,8 @@ process_chrom(const bool verbose, const std::uint32_t n_threads, const std::uint32_t n_cpgs = get_n_cpgs(epireads); if (verbose) - std::println(std::cerr, "processing {} [reads: {}][cpgs: {}]", chrom_name, - size(epireads), n_cpgs); + std::cerr << "processing " << chrom_name << " [reads: " << size(epireads) + << "][cpgs: " << n_cpgs << "]\n"; const std::uint32_t lim = n_cpgs >= window_size ? n_cpgs - window_size + 1 : 0; @@ -542,7 +541,7 @@ main_amrfinder(int argc, char *argv[]) { std::for_each(std::begin(amrs), std::end(amrs), rename_amr()); if (verbose) - std::println(std::cerr, "========= POST PROCESSING ========="); + std::cerr << "========= POST PROCESSING =========\n"; // windows_accepted is the number of sliding windows in the // methylome that were found to have a significant signal of @@ -649,7 +648,7 @@ main_amrfinder(int argc, char *argv[]) { if (!out) std::runtime_error("failed to open output file: " + outfile); for (const auto &a : amrs) - std::println(out, "{}", a); + out << to_string(a) << '\n'; } if (!summary_file.empty()) { std::ofstream summary_out(summary_file);