Repository: EvolEcolGroup/tidypopgen
Pull request #111: faster cpp vcf parser
Author: login: dramanica
Created: 2025-05-09T16:03:50Z | Merged: 2025-05-19T13:48:34Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced efficient VCF parsing functions for extracting loci metadata and converting genotype data into file-backed matrices.
  - Added a function to convert VCF files into file-backed big matrices optimized for large datasets.
- **Bug Fixes**
  - Enhanced filtering of non-biallelic SNPs during VCF processing.
- **Documentation**
  - Clarified the usage scope of the `chunk_size` parameter.
  - Updated documentation to reflect simplified function interfaces.
- **Tests**
  - Added tests to ensure correct filtering of non-biallelic SNPs and proper error handling.
  - Included new VCF test data featuring non-biallelic variants.
- **Refactor**
  - Replaced legacy VCF parsing functions with streamlined, more efficient implementations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/R/RcppExports.R b/R/RcppExports.R
index 382c0788..80c939c8 100644
--- a/R/RcppExports.R
+++ b/R/RcppExports.R
@@ -77,12 +77,12 @@ increment_king_numerator <- function(k, n_Aa_i, genotype0, genotype1, genotype2,
     invisible(.Call(`_tidypopgen_increment_king_numerator`, k, n_Aa_i, genotype0, genotype1, genotype2, genotype_valid, BM, rowInd, colInd))
 }
 
-extractAltAlleleCountsFromVCF <- function(filename, allele_counts, ploidy, numIndividuals, missingValue, maxLoci = 1000L, skipLoci = 100L, diploid = FALSE) {
-    .Call(`_tidypopgen_extractAltAlleleCountsFromVCF`, filename, allele_counts, ploidy, numIndividuals, missingValue, maxLoci, skipLoci, diploid)
+vcf_loci_table <- function(filename) {
+    .Call(`_tidypopgen_vcf_loci_table`, filename)
 }
 
-get_ploidy_from_VCF <- function(filename) {
-    .Call(`_tidypopgen_get_ploidy_from_VCF`, filename)
+vcf_genotypes_to_fbm <- function(filename, BM, biallelic, missing_value, n_header_lines) {
+    .Call(`_tidypopgen_vcf_genotypes_to_fbm`, filename, BM, biallelic, missing_value, n_header_lines)
 }
 
 write_to_FBM <- function(BM, allele_counts, col_start, n_loci, ncores) {
diff --git a/R/gen_tibble.R b/R/gen_tibble.R
index d82d7d6c..e1246a57 100644
--- a/R/gen_tibble.R
+++ b/R/gen_tibble.R
@@ -39,7 +39,7 @@
 #'   This is only used if `x` is a genotype matrix. Otherwise this information
 #'   is extracted directly from the files.
 #' @param chunk_size the number of loci or individuals (depending on the format)
-#'   processed at a time (currently used if `x` is a vcf or packedancestry file)
+#'   processed at a time (currently used if `x` is a vcf with parser "vcfR")
 #' @param ... if `x` is the name of a vcf file, additional arguments passed to
 #'   [vcfR::read.vcfR()]. Otherwise, unused.
 #' @param parser the name of the parser used for *VCF*, either "cpp" to use a
diff --git a/R/gen_tibble_vcf.R b/R/gen_tibble_vcf.R
index e54790aa..92b8cee1 100644
--- a/R/gen_tibble_vcf.R
+++ b/R/gen_tibble_vcf.R
@@ -2,19 +2,18 @@
 gen_tibble_vcf <- function(
     x,
     ...,
-    parser = "cpp",
+    parser = c("cpp", "vcfR"),
     chunk_size = NULL,
     valid_alleles = c("A", "T", "C", "G"),
     missing_alleles = c("0", "."),
     backingfile = NULL,
     quiet = FALSE) {
-  parser <- match.arg(parser, c("vcfR", "cpp"))
+  parser <- match.arg(parser)
 
   if (parser == "cpp") {
     rds_path <- vcf_to_fbm_cpp(
       x,
       backingfile = backingfile,
-      chunk_size = chunk_size,
       quiet = quiet
     )
   } else {
diff --git a/R/vcf_to_fbm_cpp.R b/R/vcf_to_fbm_cpp.R
deleted file mode 100644
index a452a4d6..00000000
--- a/R/vcf_to_fbm_cpp.R
+++ /dev/null
@@ -1,153 +0,0 @@
-#' Convert vcf to FBM.
-#'
-#' Convert a vcf file to a Filebacked Big Matrix (FBM) object. This should work
-#' even for large vcf files that would not fit in memory.
-#'
-#' @param vcf_path the path to the vcf
-#' @param chunk_size the chunk size to use on the vcf when loading the file. If
-#'   NULL, a best guess will be made.
-#' @param backingfile the name of the file to use as the backing file for the
-#'   FBM. If NULL, the vcf path will be used.
-#' @param n_cores the number of cores to use when reading the vcf file. Default
-#'   is 1.
-#' @return path to the resulting rds file as class bigSNP.
-#' @keywords internal
-
-vcf_to_fbm_cpp <- function(
-    vcf_path,
-    chunk_size = NULL,
-    backingfile = NULL,
-    n_cores = 1,
-    quiet = FALSE) {
-  if (is.null(backingfile)) {
-    backingfile <- vcf_path
-    backingfile <- sub("\\.vcf.gz$", "", backingfile)
-    backingfile <- sub("\\.vcf$", "", backingfile)
-  }
-  if (file_ext(backingfile) == "bk") {
-    backingfile <- bigstatsr::sub_bk(backingfile, "")
-  }
-
-  # figure out no_individuals and ploidy from the first marker
-  vcf_meta <- get_ploidy_from_VCF(vcf_path)
-  ploidy <- vcf_meta$ploidy
-  no_individuals <- length(ploidy)
-  max_ploidy <- max(ploidy)
-
-  # if chunk is null, get the best guess of an appropriate number
-  if (is.null(chunk_size)) {
-    chunk_size <- bigstatsr::block_size(no_individuals)
-  }
-
-  # preassign a genotype matrix that we will then fill at each pass of the vcf
-  genotypes_matrix <- matrix(
-    NA_integer_,
-    ncol = chunk_size,
-    nrow = no_individuals
-  )
-
-  # set up codes for the appropriate ploidy level
-  code256 <- rep(NA_real_, 256)
-  code256[1:(max_ploidy + 1)] <- seq(0, max_ploidy)
-
-  # metadata
-  fam <- tibble(
-    family.ID = vcf_meta$sample_names,
-    sample.ID = vcf_meta$sample_names,
-    paternal.ID = 0,
-    maternal.ID = 0,
-    sex = 0,
-    affection = -9,
-    ploidy = ploidy
-  )
-
-  loci <- tibble(
-    chromosome = NULL,
-    marker.ID = NULL,
-    physical.pos = NULL,
-    allele1 = NULL,
-    allele2 = NULL
-  )
-
-  # create the file backed matrix
-  file_backed_matrix <- bigstatsr::FBM.code256(
-    nrow = no_individuals,
-    ncol = 0,
-    code = code256,
-    backingfile = backingfile
-  )
-
-  # flag that will be changed when a chunk reaches the end of the file
-  file_end <- FALSE
-  start_locus <- 0
-
-  while (!file_end) {
-    # we need a function that reads chunk_size loci, staring from start_locus
-    # it edits the genotype_matrix, plus returns the number of valid columns
-    # (i.e. number of biallelic markers in this chunk), and a loci table
-    chunk_info <- extractAltAlleleCountsFromVCF(
-      vcf_path,
-      genotypes_matrix,
-      ploidy,
-      no_individuals,
-      max_ploidy + 1,
-      chunk_size,
-      start_locus
-    )
-    # expand the file backed matrix according to the size of the gt matrix
-    # get current size
-    index_start <- dim(file_backed_matrix)[2] + 1
-    # add the new columns
-    file_backed_matrix$add_columns(chunk_info$num_loci)
-    # fill them in
-    write_to_FBM(
-      file_backed_matrix,
-      allele_counts = genotypes_matrix,
-      col_start = index_start - 1,
-      n_loci = chunk_info$num_loci,
-      ncores = n_cores
-    )
-    # update the metadata
-    loci <- rbind(loci, chunk_info$loci_table)
-
-    # update starting point for next chunk
-    start_locus <- start_locus + chunk_size
-    # check if we are at the end of file
-    file_end <- chunk_info$end_of_file
-  }
-
-  # save it
-  file_backed_matrix$save()
-
-  # add an empty genetic.pos column
-  loci <- loci %>% mutate(genetic.dist = 0, .after = "physical.pos")
-  loci$physical.pos <- as.integer(loci$physical.pos)
-  # if loci names are missing, create a name with scaffold and position
-  # (same behaviour as vcfR)
-  missing_loci_ids <- which(loci$marker.ID == ".")
-  if (length(missing_loci_ids) > 0) {
-    loci$marker.ID[missing_loci_ids] <- paste(
-      loci$chromosome[missing_loci_ids],
-      loci$physical.pos,
-      sep = "_"
-    )
-  }
-
-  bigsnp_obj <- structure(
-    list(
-      genotypes = file_backed_matrix,
-      fam = fam,
-      map = loci
-    ),
-    class = "bigSNP"
-  )
-
-  bigsnp_obj <- bigsnpr::snp_save(bigsnp_obj)
-  # and return the path to the rds
-  bigsnp_obj$genotypes$rds
-}
-
-# get ploidy for a given individual
-get_ploidy <- function(x) {
-  max(sapply(strsplit(x, "[/|]"), function(x) length(x)))
-}
diff --git a/R/vcf_to_fbm_cpp_fast.R b/R/vcf_to_fbm_cpp_fast.R
new file mode 100644
index 00000000..25d5d26a
--- /dev/null
+++ b/R/vcf_to_fbm_cpp_fast.R
@@ -0,0 +1,92 @@
+#' Convert vcf to FBM.
+#'
+#' Convert a vcf file to a Filebacked Big Matrix (FBM) object. This should work
+#' even for large vcf files that would not fit in memory.
+#'
+#' @param vcf_path the path to the vcf
+#' @param backingfile the name of the file to use as the backing file for the
+#'   FBM. If NULL, the vcf path will be used.
+#' @return path to the resulting rds file as class bigSNP.
+#' @keywords internal
+
+vcf_to_fbm_cpp <- function(
+    vcf_path,
+    backingfile = NULL,
+    quiet = FALSE) {
+  if (is.null(backingfile)) {
+    backingfile <- vcf_path
+    backingfile <- sub("\\.vcf.gz$", "", backingfile)
+    backingfile <- sub("\\.vcf$", "", backingfile)
+  }
+  if (file_ext(backingfile) == "bk") {
+    backingfile <- bigstatsr::sub_bk(backingfile, "")
+  }
+
+  # create a loci_table, and figure out no_individuals and ploidy from first
+  # marker
+  vcf_meta <- vcf_loci_table(vcf_path)
+  # figure out no_individuals and ploidy
+  ploidy <- vcf_meta$ploidy
+  no_individuals <- length(ploidy)
+  max_ploidy <- max(ploidy)
+
+  code256 <- rep(NA_real_, 256)
+  code256[1:(max_ploidy + 1)] <- seq(0, max_ploidy)
+
+  # create the file backed matrix
+  file_backed_matrix <- bigstatsr::FBM.code256(
+    nrow = no_individuals,
+    ncol = nrow(vcf_meta$loci_tbl),
+    code = code256,
+    backingfile = backingfile
+  )
+
+  vcf_genotypes_to_fbm(vcf_path, file_backed_matrix,
+    biallelic = vcf_meta$biallelic,
+    n_header_lines = vcf_meta$n_header_lines,
+    missing_value = max_ploidy + 1
+  )
+
+
+  # individual metadata table
+  fam <- tibble(
+    family.ID = vcf_meta$sample_names,
+    sample.ID = vcf_meta$sample_names,
+    paternal.ID = 0,
+    maternal.ID = 0,
+    sex = 0,
+    affection = -9,
+    ploidy = ploidy
+  )
+
+
+  # loci metadata table
+  loci <- vcf_meta$loci_tbl
+
+  # add an empty genetic.pos column
+  loci <- loci %>% mutate(genetic.dist = 0, .after = "physical.pos")
+  loci$physical.pos <- as.integer(loci$physical.pos)
+  # if loci names are missing, create a name with scaffold and position
+  # (same behaviour as vcfR)
+  missing_loci_ids <- which(loci$marker.ID == ".")
+  if (length(missing_loci_ids) > 0) {
+    loci$marker.ID[missing_loci_ids] <- paste(
+      loci$chromosome[missing_loci_ids],
+      loci$physical.pos,
+      sep = "_"
+    )
+  }
+
+  bigsnp_obj <- structure(
+    list(
+      genotypes = file_backed_matrix,
+      fam = fam,
+      map = loci
+    ),
+    class = "bigSNP"
+  )
+
+  bigsnp_obj <- bigsnpr::snp_save(bigsnp_obj)
+  # and return the path to the rds
+  bigsnp_obj$genotypes$rds
+}
diff --git a/man/gen_tibble.Rd b/man/gen_tibble.Rd
index 09132909..c158e5ac 100644
--- a/man/gen_tibble.Rd
+++ b/man/gen_tibble.Rd
@@ -88,7 +88,7 @@ in case your
 \item{n_cores}{the number of cores to use for parallel processing}
 
 \item{chunk_size}{the number of loci or individuals (depending on the format)
-processed at a time (currently used if \code{x} is a vcf or packedancestry file)}
+processed at a time (currently used if \code{x} is a vcf with parser "vcfR")}
 
 \item{indiv_meta}{a list, data.frame or tibble with compulsory columns 'id'
 and 'population', plus any additional metadata of interest. This is only
diff --git a/man/gt_as_geno_lea.Rd b/man/gt_as_geno_lea.Rd
index 87973f77..d1b53a61 100644
--- a/man/gt_as_geno_lea.Rd
+++ b/man/gt_as_geno_lea.Rd
@@ -16,9 +16,9 @@ location of the backing files.}
 the path of the .geno file
 }
 \description{
-This function writes a .geno file from a \code{\link{gen_tibble}}. Unless a file path is
-given, a file with suffix .geno is written in the same location as the .rds
-and .bk files that underpin the \code{\link{gen_tibble}}.
+This function writes a .geno file from a \code{\link{gen_tibble}}. Unless a file path
+is given, a file with suffix .geno is written in the same location as the
+.rds and .bk files that underpin the \code{\link{gen_tibble}}.
 }
 \details{
 NOTE that we currently read all the data into memory to write the file, so
diff --git a/man/vcf_to_fbm_cpp.Rd b/man/vcf_to_fbm_cpp.Rd
index 6baa7381..c402a7dd 100644
--- a/man/vcf_to_fbm_cpp.Rd
+++ b/man/vcf_to_fbm_cpp.Rd
@@ -1,28 +1,16 @@
 % Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/vcf_to_fbm_cpp.R
+% Please edit documentation in R/vcf_to_fbm_cpp_fast.R
 \name{vcf_to_fbm_cpp}
 \alias{vcf_to_fbm_cpp}
 \title{Convert vcf to FBM.}
 \usage{
-vcf_to_fbm_cpp(
-  vcf_path,
-  chunk_size = NULL,
-  backingfile = NULL,
-  n_cores = 1,
-  quiet = FALSE
-)
+vcf_to_fbm_cpp(vcf_path, backingfile = NULL, quiet = FALSE)
 }
 \arguments{
 \item{vcf_path}{the path to the vcf}
 
-\item{chunk_size}{the chunk size to use on the vcf when loading the file. If
-NULL, a best guess will be made.}
-
 \item{backingfile}{the name of the file to use as the backing file for the
 FBM. If NULL, the vcf path will be used.}
-
-\item{n_cores}{the number of cores to use when reading the vcf file. Default
-is 1.}
 }
 \value{
 path to the resulting rds file as class bigSNP.
diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp
index f3953153..2edd8a7d 100644
--- a/src/RcppExports.cpp
+++ b/src/RcppExports.cpp
@@ -305,32 +305,29 @@ BEGIN_RCPP
     return R_NilValue;
 END_RCPP
 }
-// extractAltAlleleCountsFromVCF
-List extractAltAlleleCountsFromVCF(std::string filename, IntegerMatrix& allele_counts, IntegerVector& ploidy, int numIndividuals, int missingValue, int maxLoci, int skipLoci, bool diploid);
-RcppExport SEXP _tidypopgen_extractAltAlleleCountsFromVCF(SEXP filenameSEXP, SEXP allele_countsSEXP, SEXP ploidySEXP, SEXP numIndividualsSEXP, SEXP missingValueSEXP, SEXP maxLociSEXP, SEXP skipLociSEXP, SEXP diploidSEXP) {
+// vcf_loci_table
+List vcf_loci_table(std::string filename);
+RcppExport SEXP _tidypopgen_vcf_loci_table(SEXP filenameSEXP) {
 BEGIN_RCPP
     Rcpp::RObject rcpp_result_gen;
     Rcpp::RNGScope rcpp_rngScope_gen;
     Rcpp::traits::input_parameter< std::string >::type filename(filenameSEXP);
-    Rcpp::traits::input_parameter< IntegerMatrix& >::type allele_counts(allele_countsSEXP);
-    Rcpp::traits::input_parameter< IntegerVector& >::type ploidy(ploidySEXP);
-    Rcpp::traits::input_parameter< int >::type numIndividuals(numIndividualsSEXP);
-    Rcpp::traits::input_parameter< int >::type missingValue(missingValueSEXP);
-    Rcpp::traits::input_parameter< int >::type maxLoci(maxLociSEXP);
-    Rcpp::traits::input_parameter< int >::type skipLoci(skipLociSEXP);
-    Rcpp::traits::input_parameter< bool >::type diploid(diploidSEXP);
-    rcpp_result_gen = Rcpp::wrap(extractAltAlleleCountsFromVCF(filename, allele_counts, ploidy, numIndividuals, missingValue, maxLoci, skipLoci, diploid));
+    rcpp_result_gen = Rcpp::wrap(vcf_loci_table(filename));
     return rcpp_result_gen;
 END_RCPP
 }
-// get_ploidy_from_VCF
-List get_ploidy_from_VCF(std::string filename);
-RcppExport SEXP _tidypopgen_get_ploidy_from_VCF(SEXP filenameSEXP) {
+// vcf_genotypes_to_fbm
+bool vcf_genotypes_to_fbm(std::string filename, Environment BM, IntegerVector& biallelic, const int missing_value, const int n_header_lines);
+RcppExport SEXP _tidypopgen_vcf_genotypes_to_fbm(SEXP filenameSEXP, SEXP BMSEXP, SEXP biallelicSEXP, SEXP missing_valueSEXP, SEXP n_header_linesSEXP) {
 BEGIN_RCPP
     Rcpp::RObject rcpp_result_gen;
     Rcpp::RNGScope rcpp_rngScope_gen;
     Rcpp::traits::input_parameter< std::string >::type filename(filenameSEXP);
-    rcpp_result_gen = Rcpp::wrap(get_ploidy_from_VCF(filename));
+    Rcpp::traits::input_parameter< Environment >::type BM(BMSEXP);
+    Rcpp::traits::input_parameter< IntegerVector& >::type biallelic(biallelicSEXP);
+    Rcpp::traits::input_parameter< const int >::type missing_value(missing_valueSEXP);
+    Rcpp::traits::input_parameter< const int >::type n_header_lines(n_header_linesSEXP);
+    rcpp_result_gen = Rcpp::wrap(vcf_genotypes_to_fbm(filename, BM, biallelic, missing_value, n_header_lines));
     return rcpp_result_gen;
 END_RCPP
 }
@@ -369,8 +366,8 @@ static const R_CallMethodDef CallEntries[] = {
     {"_tidypopgen_increment_as_counts", (DL_FUNC) &_tidypopgen_increment_as_counts, 7},
     {"_tidypopgen_increment_ibs_counts", (DL_FUNC) &_tidypopgen_increment_ibs_counts, 8},
     {"_tidypopgen_increment_king_numerator", (DL_FUNC) &_tidypopgen_increment_king_numerator, 9},
-    {"_tidypopgen_extractAltAlleleCountsFromVCF", (DL_FUNC) &_tidypopgen_extractAltAlleleCountsFromVCF, 8},
-    {"_tidypopgen_get_ploidy_from_VCF", (DL_FUNC) &_tidypopgen_get_ploidy_from_VCF, 1},
+    {"_tidypopgen_vcf_loci_table", (DL_FUNC) &_tidypopgen_vcf_loci_table, 1},
+    {"_tidypopgen_vcf_genotypes_to_fbm", (DL_FUNC) &_tidypopgen_vcf_genotypes_to_fbm, 5},
     {"_tidypopgen_write_to_FBM", (DL_FUNC) &_tidypopgen_write_to_FBM, 5},
     {NULL, NULL, 0}
 };
diff --git a/src/pairwise_fst_nei87_loop.cpp b/src/pairwise_fst_nei87_loop.cpp
index bb376af8..9a4ff676 100644
--- a/src/pairwise_fst_nei87_loop.cpp
+++ b/src/pairwise_fst_nei87_loop.cpp
@@ -9,39 +9,39 @@ List pairwise_fst_nei87_loop(NumericMatrix pairwise_combn,
                       NumericMatrix freq_ref,
                       bool by_locus,
                       bool return_num_dem) {
-  
+
   int n_loci = n.nrow();
   int n_combn = pairwise_combn.ncol();
-  NumericVector fst_tot(n_combn); 
-  
+  NumericVector fst_tot(n_combn);
+
   // if we need to return fst by locus, we create the matrix, else make an empty one
   NumericMatrix fst_locus(by_locus ? n_loci : 0, by_locus ? n_combn : 0);
   // if we need to return numerator and denominator, we use fst_locus for the
   // numerator and fst_locus_dem for the denominator
   NumericMatrix fst_locus_dem(return_num_dem ? n_loci: 0, return_num_dem ? n_combn : 0);
-  
+
   for (int i_col = 0; i_col < n_combn; ++i_col) {
     int pop1 = pairwise_combn(0, i_col) -1; // Convert from R 1-based index to C++ 0-based index
     int pop2 = pairwise_combn(1, i_col) -1;
 
     NumericMatrix n_pair(n_loci, 2), sHo(n_loci, 2), freqA(n_loci, 2), freqR(n_loci, 2);
-    
+
     for (int i = 0; i < n_loci; ++i) {
       n_pair(i, 0) = n(i, pop1) / 2.0;
       n_pair(i, 1) = n(i, pop2) / 2.0;
-      
+
       sHo(i, 0) = het_obs(i, pop1);
       sHo(i, 1) = het_obs(i, pop2);
-      
+
       freqA(i, 0) = freq_alt(i, pop1);
       freqA(i, 1) = freq_alt(i, pop2);
       freqR(i, 0) = freq_ref(i, pop1);
       freqR(i, 1) = freq_ref(i, pop2);
     }
-    
+
     NumericVector mHo(n_loci), np(n_loci), mn(n_loci), msp2(n_loci), mp2(n_loci), mHs(n_loci), Ht(n_loci);
     NumericVector Dst(n_loci), Dstp(n_loci), Htp(n_loci), fst(n_loci);
-    
+
     for (int i = 0; i < n_loci; ++i) {
       // Sample size and het_obs means
       int valid = 0;
@@ -57,11 +57,11 @@ List pairwise_fst_nei87_loop(NumericMatrix pairwise_combn,
           inv_nsum += 1.0 / nij;
         }
       }
-      
+
       np[i] = valid;
       mn[i] = (inv_nsum > 0) ? nsum / inv_nsum : NA_REAL;
       mHo[i] = ho_sum / valid;
-      
+
       double sp2a = pow(freqA(i, 0), 2) + pow(freqA(i, 1), 2);
       double sp2r = pow(freqR(i, 0), 2) + pow(freqR(i, 1), 2);
       double sp2 = sp2a + sp2r;
@@ -69,16 +69,16 @@ List pairwise_fst_nei87_loop(NumericMatrix pairwise_combn,
       double freqAm = (freqA(i, 0) + freqA(i, 1)) / 2.0;
       double freqRm = (freqR(i, 0) + freqR(i, 1)) / 2.0;
       mp2[i] = pow(freqAm, 2) + pow(freqRm, 2);
-      
-      double hs1 = 1.0 - sp2 - sHo(i, 0) / (2.0 * n_pair(i, 0));
-      double hs2 = 1.0 - sp2 - sHo(i, 1) / (2.0 * n_pair(i, 1));
+
+//      double hs1 = 1.0 - sp2 - sHo(i, 0) / (2.0 * n_pair(i, 0));
+//      double hs2 = 1.0 - sp2 - sHo(i, 1) / (2.0 * n_pair(i, 1));
 
       mHs[i] = mn[i] / (mn[i] - 1.0) * (1.0 - msp2[i] - mHo[i] / (2.0 * mn[i]));
       Ht[i] = 1.0 - mp2[i] + mHs[i] / (mn[i] * np[i]) - mHo[i] / (2.0 * mn[i] * np[i]);
       Dst[i] = Ht[i] - mHs[i];
       Dstp[i] = (np[i] / (np[i] - 1.0)) * Dst[i];
       Htp[i] = mHs[i] + Dstp[i];
-      
+
       if (by_locus) {
         if (!return_num_dem) {
           fst_locus(i, i_col) =  Dstp[i] / Htp[i];
@@ -87,10 +87,10 @@ List pairwise_fst_nei87_loop(NumericMatrix pairwise_combn,
           fst_locus_dem(i, i_col) = Htp[i];
         }
       }
-      
-      
+
+
     }
-    
+
     double mean_num = 0.0, mean_denom = 0.0;
     int count = 0;
     for (int i = 0; i < n_loci; ++i) {
@@ -101,10 +101,10 @@ List pairwise_fst_nei87_loop(NumericMatrix pairwise_combn,
       }
     }
     fst_tot[i_col] = mean_num / mean_denom;
-    
+
     // fst_tot[i_col] = mean(Dstp) / mean(Htp);
   }
-  
+
   if (!return_num_dem) {
     return List::create(Named("fst_locus") = fst_locus,
                         Named("fst_tot") = fst_tot);
diff --git a/src/vcf_parser.cpp b/src/vcf_parser.cpp
deleted file mode 100644
index beeaf70c..00000000
--- a/src/vcf_parser.cpp
+++ /dev/null
@@ -1,272 +0,0 @@
-#include <Rcpp.h>
-#include <fstream>
-#include <sstream>
-#include <vector>
-#include <string>
-#include <zlib.h>
-
-using namespace Rcpp;
-
-// Function to split a string by multiple delimiters and return a vector of tokens
-std::vector<std::string> split(const std::string &s, const std::string &delimiters) {
-  std::vector<std::string> tokens;
-  size_t start = s.find_first_not_of(delimiters), end = 0;
-
-  while ((end = s.find_first_of(delimiters, start)) != std::string::npos) {
-    tokens.push_back(s.substr(start, end - start));
-    start = s.find_first_not_of(delimiters, end);
-  }
-  if (start != std::string::npos) {
-    tokens.push_back(s.substr(start));
-  }
-  return tokens;
-}
-
-// Function to count the number of alternate alleles from genotype information
-// missingValue is max_ploidy +1
-int countAlternateAlleles(const std::string &genotype, const size_t missingValue) {
-  // Rcout<<genotype<<std::endl;
-  int count = 0;
-  if (genotype[0] == '.'){
-    return missingValue;
-  }
-
-  //For odd positions, we have the allele; for even positions, we might have : for end of genotype field
-  for (size_t pos_i = 0; pos_i < genotype.size(); pos_i++){
-    if (genotype[pos_i] == '1'){
-      count++;
-    }
-
-    pos_i++;
-    // Rcout<<pos_i<<"\t"<<missingValue<<std::endl;
-    // if (genotype=="1/1"){
-    //   Rcout<<pos_i<<"\t"<<missingValue<<std::endl;
-    // }
-    if (pos_i> ((missingValue-1)*2)){
-      Rcpp::stop("a genotype has more than max_ploidy alleles. We estimate max_ploidy from the first variant in the vcf file, make sure that variant is representative of ploidy (e.g. it is not on a sex chromosome).");
-    }
-
-    //
-    if (pos_i< genotype.size()){ // don't read from string if we got beyond the limit
-      if (genotype[pos_i] == ':'){
-        break;
-      }
-    }
-
-  }
-  return count;
-}
-
-
-// Function to read a line from a gzipped file
-bool getline_gz(gzFile file, std::string &line) {
-  char buffer[1024];
-  line.clear();
-  while (gzgets(file, buffer, sizeof(buffer)) != Z_NULL) {
-    line.append(buffer);
-    if (line.back() == '\n') {
-      line.pop_back();
-      return true;
-    }
-  }
-  return !line.empty();
-}
-
-// [[Rcpp::export]]
-List extractAltAlleleCountsFromVCF(std::string filename,
-                                   IntegerMatrix& allele_counts, IntegerVector& ploidy,
-                                   int numIndividuals, int missingValue,
-                                   int maxLoci = 1000, int skipLoci = 100, bool diploid = false) {
-
-  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
-  std::ifstream vcfFile;
-  gzFile vcfGzFile;
-
-  if (is_gz) {
-    vcfGzFile = gzopen(filename.c_str(), "rb");
-    if (vcfGzFile == NULL) {
-      stop("Error opening gzipped file");
-    }
-  } else {
-    vcfFile.open(filename);
-    if (!vcfFile.is_open()) {
-      stop("Error opening file");
-    }
-  }
-
-  // metadata
-  std::vector<std::string>chromosome;
-  chromosome.reserve(maxLoci);
-  std::vector<std::string> marker_id;
-  marker_id.reserve(maxLoci);
-  std::vector<std::string> physical_pos;
-  physical_pos.reserve(maxLoci);
-  std::vector<std::string> allele1;
-  allele1.reserve(maxLoci);
-  std::vector<std::string> allele2;
-  allele2.reserve(maxLoci);
-
-  std::string line;
-  std::string this_chromosome = ""; // chromosome of the current line
-  int lociCount = 0; // number of biallelic loci
-  int lineCount = 0; // number of valid lines
-  int skippedLoci = 0;
-  bool end_of_file = false;
-
-  // Read the file line by line
-  while (lineCount < maxLoci) {
-    bool gotLine;
-    if (is_gz) {
-      gotLine = getline_gz(vcfGzFile, line);
-    } else {
-      gotLine = (bool)std::getline(vcfFile, line);
-    }
-    if (!gotLine) {
-      end_of_file = true; // we got to the end of the file
-      break;
-    }
-
-    // Skip header lines
-    if (line[0] == '#') {
-      continue;
-    }
-
-    // Skip the specified number of loci
-    if (skippedLoci < skipLoci) {
-      skippedLoci++;
-      continue;
-    }
-
-    // Split the VCF line into fields
-    std::vector<std::string> fields = split(line, "\t");
-
-    // Check if the marker is biallelic SNP (REF and ALT allele are single character)
-    if ((fields[3].length() ==1) && (fields[4].length() ==1)) {
-      chromosome.push_back(fields[0]);
-      physical_pos.push_back(fields[1]);
-      marker_id.push_back(fields[2]);
-      allele1.push_back(fields[4]);
-      allele2.push_back(fields[3]);
-
-      // Collect alternate allele counts for all individuals
-      if (fields[4][0] != '.'){
-        for (unsigned int i = 9; i < fields.size(); ++i) {
-          int count = countAlternateAlleles(fields[i], missingValue);
-          allele_counts(i - 9, lociCount) = count;
-        }
-      } else { // if this is an invariant site
-        for (unsigned int i = 9; i < fields.size(); ++i) {
-          allele_counts(i - 9, lociCount) = 0;
-        }
-      }
-
-
-      lociCount++;
-    }
-    lineCount++;
-  }
-  // test if the next line would have been the end of file
-  bool gotLine;
-  if (is_gz) {
-    gotLine = getline_gz(vcfGzFile, line);
-  } else {
-    gotLine = (bool)std::getline(vcfFile, line);
-  }
-  if (!gotLine) {
-    end_of_file = true; // we got to the end of the file
-  }
-
-  if (is_gz) {
-    gzclose(vcfGzFile);
-  } else {
-    vcfFile.close();
-  }
-
-  // craete loci table
-  DataFrame loci_table = DataFrame::create( Named("chromosome") = chromosome ,
-                                            _["marker.ID"] = marker_id,
-                                            _["physical.pos"] = physical_pos,
-                                            _["allele1"] = allele1,
-                                            _["allele2"] = allele2);
-
-  // Return a list with useful information
-  return List::create(
-    Named("loci_table") = loci_table,
-    Named("num_loci") = lociCount,
-    Named("end_of_file") = end_of_file
-  );
-}
-
-
-// get ploid of each individual from the first line of data
-// [[Rcpp::export]]
-List get_ploidy_from_VCF(std::string filename) {
-  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
-  std::ifstream vcfFile;
-  gzFile vcfGzFile;
-
-  if (is_gz) {
-    vcfGzFile = gzopen(filename.c_str(), "rb");
-    if (vcfGzFile == NULL) {
-      stop("Error opening gzipped file");
-    }
-  } else {
-    vcfFile.open(filename);
-    if (!vcfFile.is_open()) {
-      stop("Error opening file");
-    }
-  }
-
-  std::string line;
-  // when we find the genotypes, we need to look at the old line for the names of samples
-  std::string line_old;
-  int numIndividuals = 0;
-  std::vector<int> ploidy;
-  std::vector<std::string> sample_names;
-
-  // Read the file line by line
-  while (true) {
-    bool gotLine;
-    if (is_gz) {
-      gotLine = getline_gz(vcfGzFile, line);
-    } else {
-      gotLine = (bool)std::getline(vcfFile, line);
-    }
-    if (!gotLine) {
-      break;
-    }
-
-    // Skip header lines
-    if (line[0] == '#') {
-      line_old = line;
-      continue;
-    }
-
-    // Split the VCF line into fields
-    std::vector<std::string> fields = split(line, "\t");
-    numIndividuals = fields.size() - 9; // 9 fixed fields before individual data
-    ploidy.resize(numIndividuals, 0);
-    for (unsigned int i = 9; i < fields.size(); ++i) {
-      ploidy[i - 9] = split(fields[i], "/|").size();
-      //ploidy[i - 9] = 1+ (fields[i].length()-1)/2;
-    }
-    // now process names
-    fields = split(line_old, "\t");
-    sample_names.resize(numIndividuals,"");
-    for (unsigned int i = 9; i < fields.size(); ++i) {
-      sample_names[i - 9] = fields[i];
-    }
-    break;
-  }
-
-  if (is_gz) {
-    gzclose(vcfGzFile);
-  } else {
-    vcfFile.close();
-  }
-
-  // Return a list containing the matrix and the ploidy vector
-  return (List::create(Named("sample_names") = sample_names,
-                       Named("ploidy") = ploidy));
-}
-
diff --git a/src/vcf_parser_fast.cpp b/src/vcf_parser_fast.cpp
new file mode 100644
index 00000000..4fae6dfc
--- /dev/null
+++ b/src/vcf_parser_fast.cpp
@@ -0,0 +1,355 @@
+#include <Rcpp.h>
+#include <fstream>
+#include <sstream>
+#include <vector>
+#include <string>
+#include <zlib.h>
+#include <bigstatsr/BMCodeAcc.h>
+
+using namespace Rcpp;
+using namespace std;
+
+// Function to read a line from a gzipped file
+inline bool gz_getline(gzFile file, std::string &line) {
+  char buffer[1024];
+  line.clear();
+  while (gzgets(file, buffer, sizeof(buffer)) != Z_NULL) {
+    line.append(buffer);
+    if (line.back() == '\n') {
+      line.pop_back();
+      return true;
+    }
+  }
+  return !line.empty();
+}
+
+
+// Function to split a string by multiple delimiters and return a vector of tokens
+inline std::vector<std::string> split_line(const std::string &s, const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+  
+  while ((end = s.find_first_of(delimiters, start)) != std::string::npos) {
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  if (start != std::string::npos) {
+    tokens.push_back(s.substr(start));
+  }
+  return tokens;
+}
+
+// Function to split the metadata for a locus and return a vector of tokens
+// this only needs to read the first 5 elements in the string
+inline std::vector<std::string> split_locus_meta(const std::string &s,
+                                                 const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+  
+  for (int i = 0; i < 5; i++) {
+    end = s.find_first_of(delimiters, start);
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  return tokens;
+}
+
+
+
+/******************************************************************************
+ * Create a loci table and get ploidy and number of individuals from vcf
+ *
+ * This function parses the VCF for biallelic snps, and creates a loci table
+ * with information for those snps. It also creates a boolean vector to indicate
+ * which loci are biallelic, and a vector with the ploidy of each individual.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+// [[Rcpp::export]]
+List vcf_loci_table(std::string filename) {
+  // open the file
+  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
+  std::ifstream vcfFile;
+  gzFile vcfGzFile;
+  
+  if (is_gz) {
+    vcfGzFile = gzopen(filename.c_str(), "rb");
+    if (vcfGzFile == NULL) {
+      stop("Error opening gzipped file");
+    }
+  } else {
+    vcfFile.open(filename);
+    if (!vcfFile.is_open()) {
+      stop("Error opening file");
+    }
+  }
+  
+  // is a locus biallelic
+  std::vector<bool> biallelic;
+  biallelic.reserve(10e6);
+  // loci metadata (preallocate a million loci, as a start)
+  std::vector<std::string>chromosome;
+  chromosome.reserve(10e6);
+  std::vector<std::string> marker_id;
+  marker_id.reserve(10e6);
+  std::vector<int32_t> physical_pos;
+  physical_pos.reserve(10e6);
+  std::vector<std::string> allele1;
+  allele1.reserve(10e6);
+  std::vector<std::string> allele2;
+  allele2.reserve(10e6);
+  // individual metadata
+  std::vector<int> ploidy;
+  std::vector<std::string> sample_names;
+  
+  
+  std::string line;
+  std::string line_old; // used to get the samples from the last line of the header
+  int n_header_lines =0;
+  
+  bool get_sample_names = true;
+  bool get_ploidy = true;
+  
+  // Read the file line by line until the end of file
+  while (true) {
+    bool gotLine;
+    if (is_gz) {
+      gotLine = gz_getline(vcfGzFile, line);
+    } else {
+      gotLine = (bool)std::getline(vcfFile, line);
+    }
+    
+    // Check if we reached the end of the file
+    if (!gotLine) {
+      break;
+    }
+    
+    // Skip header lines
+    if (line[0] == '#') {
+      line_old = line; // save the last line of the header
+      n_header_lines++;
+      continue;
+    }
+    
+    // Check if we need to get the sample names (this is only the case for
+    // the first line after headers)
+    if (get_sample_names) {
+      std::vector<std::string> fields = split_line(line_old, "\t");
+      for (unsigned int i = 9; i < fields.size(); ++i) {
+        sample_names.push_back(fields[i]);
+      }
+      get_sample_names = false;
+    }
+    
+    // Extract chromosome, position, and
+    // Split the VCF line into fields (only for the first few fields)
+    std::vector<std::string> fields = split_locus_meta(line, "\t");
+    
+    // Check if the marker is biallelic SNP (REF and ALT allele are single character)
+    // if yes, get the metadata for this locus
+    if ((fields[3].length() ==1) && (fields[4].length() ==1)) {
+      biallelic.push_back(true);
+      chromosome.push_back(fields[0]);
+      physical_pos.push_back(std::stoi(fields[1]));
+      marker_id.push_back(fields[2]);
+      allele1.push_back(fields[4]);
+      allele2.push_back(fields[3]);
+    } else {
+      biallelic.push_back(false);
+    }
+    
+    
+    // Check if we need to get ploidy (this is only the case for the first line
+    // of real data)
+    if (get_ploidy) {
+      std::vector<std::string> fields = split_line(line, "\t");
+      for (unsigned int i = 9; i < fields.size(); ++i) {
+        ploidy.push_back(split_line(fields[i], "/|").size());
+      }
+      get_ploidy = false;
+    }
+    
+  }
+  
+  // close the file
+  if (is_gz) {
+    gzclose(vcfGzFile);
+  } else {
+    vcfFile.close();
+  }
+  
+  // craete loci table
+  DataFrame loci_table = DataFrame::create( Named("chromosome") = chromosome ,
+                                            _["marker.ID"] = marker_id,
+                                            _["physical.pos"] = physical_pos,
+                                            _["allele1"] = allele1,
+                                            _["allele2"] = allele2);
+  
+  
+  // Return a list containing the matrix and the ploidy vector
+  return (List::create(Named("loci_tbl") = loci_table,
+                       Named("sample_names") = sample_names,
+                       Named("ploidy") = ploidy,
+                       Named("biallelic") = biallelic,
+                       Named("n_header_lines") = n_header_lines));
+}
+
+
+// Function to count the number of alternate alleles from genotype information
+//
+/******************************************************************************
+ * count the number of alternate alleles from genotype information
+ *
+ * missingValue is max_ploidy +1
+ *
+ * @param filename The name of the VCF file to parse
+ * This function parses a line of the VCF and counts the number of alternate alleles,
+ * storing it in an array of raw values (than can then be written directly to the
+ * file backed matrix).
+ *
+ * @param string line The line of the VCF file to parse
+ * @param unsigned char* alt_counts The array to store the counts
+ * @param int n The number of individuals
+ * @param int missingValue The value to use for missing genotypes
+ */
+
+inline void count_alt_alleles(const std::string &line,
+                              unsigned char* alt_counts,
+                              size_t* separator_pos,
+                              size_t n,
+                              const size_t missingValue,
+                              const std::string &separator
+) {
+  // reset counts to 0
+  std::fill(alt_counts, alt_counts+n, 0);
+  // for (uint i = 0; i < n; i++) {
+  //   alt_counts[i] = 0;
+  // }
+  
+  separator_pos[0] = line.find(separator, 0);
+  // find the position of all separators
+  for (size_t i = 1; i < 8+n; i++) {
+    separator_pos[i] = line.find(separator, separator_pos[i-1]+1);
+  }
+  separator_pos[8+n] = line.length();
+  
+  // now move through each of the genotypes and process them
+  // we ignore the first 9 fields (so start from position 9)
+  for (size_t i=0; i<n; i++){
+    size_t pos = separator_pos[i+8];
+    // the genotypes start at pos +1
+    if (line[pos+1] == '.') {
+      alt_counts[i] = missingValue;
+    } else {
+      for (size_t pos_i = pos+1; pos_i < separator_pos[i+9]+1; pos_i++){
+        if (line[pos_i] == '1'){
+          alt_counts[i]++;
+        } else if (line[pos_i] == ':') //if at the end of the genotype section
+          break;
+      }
+    }
+  }
+}
+
+
+
+
+
+/******************************************************************************
+ * Read the genotypes into a File Backed Matrix from a vcf
+ *
+ * This function parses the VCF, using information gathered from vcf_loci_table.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+
+// [[Rcpp::export]]
+bool vcf_genotypes_to_fbm(std::string filename,
+                          Environment BM,
+                          IntegerVector& biallelic,
+                          const int missing_value,
+                          const int n_header_lines) {
+  // open the file
+  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
+  std::ifstream vcfFile;
+  gzFile vcfGzFile;
+  
+  if (is_gz) {
+    vcfGzFile = gzopen(filename.c_str(), "rb");
+    if (vcfGzFile == NULL) {
+      stop("Error opening gzipped file");
+    }
+  } else {
+    vcfFile.open(filename);
+    if (!vcfFile.is_open()) {
+      stop("Error opening file");
+    }
+  }
+  
+  // set up access to the file backed matrix
+  XPtr<FBM_RW> xpBM = BM["address_rw"];
+  unsigned char* ptr = static_cast<unsigned char*>(xpBM->matrix());
+  //  const unsigned char* code_ptr;
+  int n = xpBM->nrow(); // number of individuals
+  
+  
+  // total number of loci in the vcf
+  int n_loci = biallelic.size(); // number of loci
+  
+  // string to store the line read from the file
+  std::string line;
+  std::string tab_delimiter = "\t";
+  
+  // Skip the header
+  for (int i = 0; i < n_header_lines; i++) {
+    if (is_gz) {
+      gz_getline(vcfGzFile, line);
+    } else {
+      std::getline(vcfFile, line);
+    }
+  }
+  // array to store the genotypes
+  unsigned char* alt_counts = new unsigned char[n];
+  // position of the separator, the last value is the end of the string
+  size_t* separator_pos = new size_t[n+9];
+  
+  // Read the genotypes line by line until the end of file
+  for (int i_geno = 0; i_geno < n_loci; i_geno++) {
+    bool gotLine;
+    if (is_gz) {
+      gotLine = gz_getline(vcfGzFile, line);
+    } else {
+      gotLine = (bool)std::getline(vcfFile, line);
+    }
+    // Check if we reached the end of the file
+    if (!gotLine) {
+      stop("end of file reached before all expected genotypes were read");
+    }
+    if (biallelic[i_geno]){
+      count_alt_alleles(line, &alt_counts[0], &separator_pos[0], n,missing_value, tab_delimiter);
+      ptr = std::copy(&alt_counts[0], &alt_counts[0] + n, ptr);
+    }
+    
+  }
+  // check ploidy for the last line, in case we estimated it incorrectly in the first
+  // line
+  std::vector<std::string> fields = split_line(line, "\t");
+  for (unsigned int i = 9; i < fields.size(); ++i) {
+    if (split_line(fields[i], "/|").size() > size_t((missing_value-1))) {
+      stop("a genotype has more than max_ploidy alleles. We estimate max_ploidy from the first variant in the vcf file, make sure that variant is representative of ploidy (e.g. it is not on a sex chromosome)");
+    }
+  }
+  
+  // close the file
+  if (is_gz) {
+    gzclose(vcfGzFile);
+  } else {
+    vcfFile.close();
+  }
+  // delete the array
+  delete[] alt_counts;
+  delete[] separator_pos;
+  // return true if the file was read successfully
+  return true;
+  
+}
diff --git a/tests/testthat/test_gen_tibble.R b/tests/testthat/test_gen_tibble.R
index 75f4de88..df8895cc 100644
--- a/tests/testthat/test_gen_tibble.R
+++ b/tests/testthat/test_gen_tibble.R
@@ -584,8 +584,8 @@ test_that("gentibble with packedancestry", {
   # individuals are homozygous ref. This is the case for snps rs9697457,
   # rs2862633, rs28569024.
   expect_true(all.equal(
-    show_loci(pop_a_bed_gt)[, (names(show_loci(pop_a_bed_gt)) %in% c("big_index", "name", "chromosome", "position", "allele_ref", "chr_int"))], #nolint
-    show_loci(pop_a_gt)[, (names(show_loci(pop_a_gt)) %in% c("big_index", "name", "chromosome", "position", "allele_ref", "chr_int"))] #nolint
+    show_loci(pop_a_bed_gt)[, (names(show_loci(pop_a_bed_gt)) %in% c("big_index", "name", "chromosome", "position", "allele_ref", "chr_int"))], # nolint
+    show_loci(pop_a_gt)[, (names(show_loci(pop_a_gt)) %in% c("big_index", "name", "chromosome", "position", "allele_ref", "chr_int"))] # nolint
   ))
 })
 
@@ -655,8 +655,8 @@ test_that("gentibble with packedancestry and missingness", {
   # individuals are homozygous ref. This is the case for snps rs9697457,
   # rs2862633, rs28569024.
   expect_true(all.equal(
-    show_loci(pop_b_bed_gt)[, (names(show_loci(pop_b_bed_gt)) %in% c("big_index", "name", "chromosome", "position", "allele_ref", "chr_int"))], #nolint
-    show_loci(pop_b_gt)[, (names(show_loci(pop_b_gt)) %in% c("big_index", "name", "chromosome", "position", "allele_ref", "chr_int"))] #nolint
+    show_loci(pop_b_bed_gt)[, (names(show_loci(pop_b_bed_gt)) %in% c("big_index", "name", "chromosome", "position", "allele_ref", "chr_int"))], # nolint
+    show_loci(pop_b_gt)[, (names(show_loci(pop_b_gt)) %in% c("big_index", "name", "chromosome", "position", "allele_ref", "chr_int"))] # nolint
   ))
 })
 
diff --git a/tests/testthat/test_gt_tibble_vcf.R b/tests/testthat/test_gt_tibble_vcf.R
new file mode 100644
index 00000000..afca06ea
--- /dev/null
+++ b/tests/testthat/test_gt_tibble_vcf.R
@@ -0,0 +1,33 @@
+test_that("gen_tibble_vcf filters non-biallelic SNPs", {
+  # get path for non-biallelic SNPs
+  vcf_path <- test_path("testdata/vcf", "pop_a_non_biallelic.vcf")
+  # read in the vcf
+  vcf_cpp_gt <- gen_tibble(
+    x = vcf_path,
+    parser = "cpp",
+    backingfile = tempfile(),
+    quiet = TRUE
+  )
+  pop_a_gt <-
+    gen_tibble(
+      system.file("extdata/pop_a.vcf", package = "tidypopgen"),
+      backingfile = tempfile(),
+      quiet = TRUE
+    )
+  # the vcf is missing locus rs3094315 and rs 307354
+  expect_true(all(!c("rs3094315", "rs307354") %in% show_loci(vcf_cpp_gt)$name))
+  # check that the genotypes are the same
+  # remove them from the pop_a_gt tibble
+  pop_a_gt_sub <- pop_a_gt %>% select_loci(!any_of(c("rs3094315", "rs307354")))
+  # and check
+  expect_true(all(show_genotypes(pop_a_gt_sub) == show_genotypes(vcf_cpp_gt)))
+
+  # note that our vcfr does not cope with biallelic non-SNPs
+  # we catch an error as one of the alleles is not a valid allele
+  expect_error(vcf_vcfr_gt <- gen_tibble(
+    x = vcf_path,
+    parser = "vcfR",
+    backingfile = tempfile(),
+    quiet = TRUE
+  ), "valid alleles are ")
+})
diff --git a/tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf b/tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf
new file mode 100644
index 00000000..eaf5969f
--- /dev/null
+++ b/tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf
@@ -0,0 +1,26 @@
+##fileformat=VCFv4.2
+##fileDate=20240510
+##source=PLINKv1.90
+##contig=<ID=1,length=1346906>
+##contig=<ID=2,length=235832764>
+##contig=<ID=3,length=155913652>
+##contig=<ID=23,length=51433072>
+##INFO=<ID=PR,Number=0,Type=Flag,Description="Provisional reference allele, may not be based on real reference genome">
+##FORMAT=<ID=GT,Number=1,Type=String,Description="Genotype">
+#CHROM	POS	ID	REF	ALT	QUAL	FILTER	INFO	FORMAT	pop_a_GRC14300079	pop_a_GRC14300142	pop_a_GRC14300159	pop_a_GRC14300286	pop_a_GRC14300305
+1	82154	rs4477212	A	.	.	.	PR	GT	0/0	0/0	0/0	0/0	0/0
+1	752566	rs3094315	A	G,T	.	.	PR	GT	0/2	0/0	0/1	0/0	0/0
+1	752721	rs3131972	G	A	.	.	PR	GT	0/0	0/0	0/1	0/0	0/1
+1	776546	rs12124819	A	.	.	.	PR	GT	0/0	0/0	0/0	0/0	0/0
+1	798959	rs11240777	G	A	.	.	PR	GT	0/0	0/0	1/1	0/0	0/1
+1	873558	rs1110052	G	T	.	.	PR	GT	0/1	0/0	0/1	0/0	1/1
+1	934345	rs9697457	G	.	.	.	PR	GT	0/0	0/0	0/0	0/0	0/0
+1	957640	rs6657048	C	.	.	.	PR	GT	0/0	0/0	0/0	0/0	0/0
+1	994391	rs2488991	T	.	.	.	PR	GT	0/0	0/0	0/0	0/0	0/0
+1	1264539	rs307354	C	GTCT	.	.	PR	GT	0/0	0/1	0/0	0/1	0/0
+1	1346905	rs1240719	T	A	.	.	PR	GT	0/0	0/1	0/0	0/0	0/0
+2	61974443	rs2862633	G	.	.	.	PR	GT	0/0	0/0	0/0	0/0	0/0
+2	139008811	rs28569024	T	.	.	.	PR	GT	0/0	0/0	0/0	0/0	0/0
+2	235832763	rs10106770	G	A	.	.	PR	GT	0/1	0/0	0/1	0/1	0/1
+3	155913651	rs11942835	T	C	.	.	PR	GT	0/0	0/1	0/0	0/1	0/0
+23	51433071	rs5945676	T	.	.	.	PR	GT	0	0	0	0	0


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (2)</summary><blockquote>

<details>
<summary>R/vcf_to_fbm_cpp_fast.R (2)</summary><blockquote>

`27-29`: _⚠️ Potential issue_

**`file_ext()` requires an explicit namespace**

`file_ext()` lives in the `tools` package, which is not attached by default.  
Call it as `tools::file_ext()` (or `fs::path_ext()` if you depend on `fs`) to prevent a runtime “could not find function ‘file_ext’” when the caller does not have `tools` on the search path.

```diff
-  if (file_ext(backingfile) == "bk") {
+  if (tools::file_ext(backingfile) == "bk") {
```

---

`16-26`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**`chunk_size`, `n_cores` and `quiet` are accepted but ignored**

These arguments are documented yet never referenced in the body of `vcf_to_fbm_cpp_new()`.  
Either wire them to the underlying C++ code (e.g. pass `n_cores` for parallel-friendly parsing) or drop them from the signature/documentation to avoid a confusing API.

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the file defining vcf_to_fbm_cpp_new
file=$(rg "vcf_to_fbm_cpp_new" -l .)
echo "File: $file"

# Display the first 200 lines of the file to inspect function body
echo "---- File content (first 200 lines) ----"
sed -n '1,200p' "$file"

# Search for usages of the arguments within the function
echo "---- Searching for 'chunk_size' usage ----"
grep -R "chunk_size" -n "$file" || echo "No occurrences of 'chunk_size' found in function body"
echo "---- Searching for 'n_cores' usage ----"
grep -R "n_cores" -n "$file" || echo "No occurrences of 'n_cores' found in function body"
echo "---- Searching for 'quiet' usage ----"
grep -R "quiet" -n "$file" || echo "No occurrences of 'quiet' found in function body"
```

Length of output: 1845

---

🏁 Script executed:

```shell
#!/bin/bash
# Inspect vcf_to_fbm_cpp_new definition and usage of chunk_size, n_cores, quiet in R/vcf_to_fbm_cpp_fast.R
file="R/vcf_to_fbm_cpp_fast.R"
echo "Definition location in $file:"
grep -R -n "vcf_to_fbm_cpp_new" "$file"
lineno=$(grep -R -n "vcf_to_fbm_cpp_new" "$file" | cut -d: -f1)
start=$((lineno))
end=$((lineno+100))
echo "---- ${file}:${start}-${end} ----"
sed -n "${start},${end}p" "$file"
echo "---- Usage of chunk_size, n_cores, quiet in this block ----"
sed -n "${start},${end}p" "$file" | grep -n -E "chunk_size|n_cores|quiet" || echo "No occurrences of these arguments found"
```

Length of output: 2890

---



</details>

**Remove or implement unused parameters in `vcf_to_fbm_cpp_new`**

The arguments `chunk_size`, `n_cores` and `quiet` are declared in the function signature but never used in the body, leading to a confusing API.

Please address this in R/vcf_to_fbm_cpp_fast.R:
- File: R/vcf_to_fbm_cpp_fast.R  
  Signature at lines 16–20:
  ```r
  vcf_to_fbm_cpp_new <- function(
      vcf_path,
      chunk_size = NULL,
      backingfile = NULL,
      n_cores = 1,
      quiet = FALSE) { … }
  ```
- No further references to `chunk_size`, `n_cores` or `quiet` appear in lines 16–116.

Action items:
- Either wire these parameters through to the C++/parsing layer (e.g. use `chunk_size` for chunked reads, pass `n_cores` to a parallel parser, honor `quiet` to suppress messages),  
- Or remove them (and update documentation) to avoid misleading callers.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>R/RcppExports.R (1)</summary><blockquote>

`56-62`: **Add missing roxygen tags for the new wrappers**

`vcf_loci_table()` and `vcf_genotypes_to_fbm()` are now part of the public R interface, but the autogenerated `RcppExports.R` file does not include their roxygen documentation stubs.  
If the functions are meant to be user-facing, remember to regenerate the roxygen skeletons ( `Rcpp::compileAttributes()` ) and commit the corresponding `@export` entries in the `.R` sources.  
Otherwise they will not appear in the help index and package checks (`R CMD check`) will raise “missing alias” notes.

</blockquote></details>
<details>
<summary>R/vcf_to_fbm_cpp_fast.R (1)</summary><blockquote>

`39-41`: **Off-by-one on the NA code is intentional but deserves an inline comment**

`missing_value <- max_ploidy + 1` deliberately maps to an **NA** slot in `code256`, relying on uninitialised indices to remain `NA`.  
Consider documenting this subtle convention in-line for future maintainers so that they do not “fix” the apparent gap.

</blockquote></details>
<details>
<summary>src/vcf_parser_fast.cpp (2)</summary><blockquote>

`213-224`: **Replace manual zeroing with `std::fill` (micro-optimisation)**

The explicit `for` loop resetting `alt_counts` to zero can be replaced by `std::fill(alt_counts, alt_counts + n, 0);` for clarity and potential compiler optimisation.

---

`222-223`: **Non-portable alias: `uint`**

`uint` is not guaranteed to be defined by the standard headers.  
Use `std::size_t` or `unsigned int` to avoid build failures on stricter compilers.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a58f4f9e5e53787c4970bdfe2a08b632f9640f6e and 49b1ad3fe36b20b91436b3eb6841381f1a360abb.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `R/RcppExports.R` (1 hunks)
* `R/gen_tibble.R` (1 hunks)
* `R/gen_tibble_vcf.R` (1 hunks)
* `R/vcf_to_fbm_cpp_fast.R` (3 hunks)
* `man/gen_tibble.Rd` (1 hunks)
* `man/vcf_to_fbm_cpp_new.Rd` (1 hunks)
* `src/RcppExports.cpp` (2 hunks)
* `src/vcf_parser.cpp` (0 hunks)
* `src/vcf_parser_fast.cpp` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* src/vcf_parser.cpp

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/RcppExports.cpp (1)</summary><blockquote>

<details>
<summary>src/vcf_parser_fast.cpp (4)</summary>

* `vcf_loci_table` (69-192)
* `vcf_loci_table` (69-69)
* `vcf_genotypes_to_fbm` (263-346)
* `vcf_genotypes_to_fbm` (263-267)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: ubuntu-latest (oldrel-1)
* GitHub Check: ubuntu-latest (release)
* GitHub Check: ubuntu-latest (devel)
* GitHub Check: pkgdown
* GitHub Check: windows-latest (release)
* GitHub Check: macos-latest (release)
* GitHub Check: test-coverage
* GitHub Check: lint

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>man/gen_tibble.Rd (1)</summary>

`91-91`: **Clear documentation update.**

The documentation for `chunk_size` has been appropriately updated to clarify that it's only applicable when using the "vcfR" parser for VCF files, which aligns with the changes to the underlying parsing implementation.

</details>
<details>
<summary>R/gen_tibble.R (1)</summary>

`42-42`: **Documentation updated correctly.**

The roxygen documentation for `chunk_size` is now properly aligned with the actual implementation. This clarifies for users that chunk processing is only applicable when using the "vcfR" parser.

</details>
<details>
<summary>man/vcf_to_fbm_cpp_new.Rd (2)</summary>

`2-4`: **Function documentation properly renamed.**

The function name and reference path have been correctly updated to reflect the new implementation. This ensures that the documentation stays in sync with the refactored code.

---

`7-7`: **Updated usage documentation.**

The usage section has been properly updated to show the new function name.

</details>
<details>
<summary>R/gen_tibble_vcf.R (2)</summary>

`5-5`: **Improved parser argument handling.**

The function now correctly accepts both "cpp" and "vcfR" as parser options, and properly validates the input using `match.arg()`. This is a robust approach that ensures only valid parser options are used.



Also applies to: 11-11

---

`14-14`: <details>
<summary>❓ Verification inconclusive</summary>

**Updated function call to new implementation.**

The function now calls the new `vcf_to_fbm_cpp_new` implementation instead of the previous version, which should provide the performance improvements described in the PR objectives.


I recommend verifying that the new implementation maintains compatibility with the same input parameters, especially since the PR mentions a shift from chunked incremental reading to a single-pass approach.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the parameters for vcf_to_fbm_cpp_new match the expected usage
grep -rn "function(" --include="R/vcf_to_fbm_cpp_fast.R" . | head -1
```

Length of output: 68

---

I couldn’t find the new function definition with the previous command. Let’s locate where `vcf_to_fbm_cpp_new` is defined and inspect its parameter list:

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the R file that defines vcf_to_fbm_cpp_new
rg "vcf_to_fbm_cpp_new *<- *function" -n --include '*.R'
```

Length of output: 500

---

Let’s broaden the search to find where `vcf_to_fbm_cpp_new` is defined:

---

🏁 Script executed:

```shell
#!/bin/bash
# 1. Search all R files with grep
grep -rn --include='*.R' "vcf_to_fbm_cpp_new *<- *function" .

# 2. Use fd to locate R files, then grep
fd --extension R . | xargs grep -n "vcf_to_fbm_cpp_new *<- *function"
```

Length of output: 362

---

Let’s compare the parameter lists of both implementations to ensure compatibility:

---

🏁 Script executed:

```shell
#!/bin/bash
# 1. Show signature of the new implementation
echo "=== vcf_to_fbm_cpp_new signature ==="
awk 'NR>=16 && NR<=30' R/vcf_to_fbm_cpp_fast.R

# 2. Locate and show signature of the old implementation
OLD_FILE=$(grep -rl --include='*.R' "vcf_to_fbm_cpp *<- *function" . | grep -v vcf_to_fbm_cpp_fast.R | head -n1)
echo "=== Old implementation in $OLD_FILE ==="
awk 'NR>=1 && NR<=30' "$OLD_FILE"
```

Length of output: 743

---



</details>

**Unable to locate old `vcf_to_fbm_cpp` signature – manual verification needed**

I couldn’t find the original R definition of `vcf_to_fbm_cpp` to compare its parameters against the new implementation’s signature:

  • vcf_to_fbm_cpp_new(vcf_path, chunk_size = NULL, backingfile = NULL, n_cores = 1, quiet = FALSE)

Please confirm that these arguments match the old function’s signature and behavior, especially around `chunk_size` and `backingfile`.

</details>
<details>
<summary>src/RcppExports.cpp (3)</summary>

`213-223`: **New function for efficient VCF metadata extraction looks good**

This function provides a more streamlined approach to extracting metadata from VCF files compared to the previous implementation. It efficiently parses the file, extracts sample names, biallelic flags, ploidy information, and loci metadata in a single pass.

---

`224-238`: **New function for direct genotype extraction to FBM is well implemented**

This function efficiently reads genotype data directly into a file-backed matrix (FBM), avoiding intermediate memory allocations. The signature accepts all necessary parameters including handling for missing values and header lines, and returns a boolean success indicator.

---

`268-269`: **Correctly updated CallEntries with new functions**

The CallEntries array has been properly updated to include the new functions with their correct parameter counts (1 for vcf_loci_table and 5 for vcf_genotypes_to_fbm).

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T16:09:15Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>src/vcf_parser_fast.cpp (1)</summary><blockquote>

`314-330`: _⚠️ Potential issue_

**Loop stops after `n_biallelic` lines, skipping earlier non-biallelic variants**

`for (int i_geno = 0; i_geno < n_loci; i_geno++)` assumes the first `n_loci`
variant lines are all represented in `biallelic`.  
If the VCF starts with any multiallelic sites, the FBM remains empty because
`biallelic[0..k] == FALSE`, yet the counter still advances.

Refactor to iterate until the FBM is full, incrementing a separate
`loci_written` counter only when a biallelic locus is copied:

```diff
-  for (int i_geno = 0; i_geno < n_loci; i_geno++) {
+  size_t file_idx = 0, loci_written = 0;
+  while (loci_written < static_cast<size_t>(n_loci)) {
     ...
-    if (biallelic[i_geno]) {
+    if (biallelic[file_idx]) {
       ...
       loci_written++;
     }
+    file_idx++;
   }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>man/vcf_to_fbm_cpp.Rd (1)</summary><blockquote>

`7-14`: **Missing documentation for the `quiet` parameter**

The `usage` section shows `quiet = FALSE`, but `\arguments{}` does not describe it.  
Please add a short explanation so users know it suppresses messages during parsing.

</blockquote></details>
<details>
<summary>src/vcf_parser_fast.cpp (3)</summary><blockquote>

`88-100`: **Magic constant & implicit double → size_t conversion**

`reserve(10e6)` is parsed as a `double` literal then cast to `size_t`.  
Prefer an explicit integer literal and document why 10 million is chosen.

```cpp
static const size_t PREALLOC_LOCI = 10'000'000;   // ~10 M loci
biallelic.reserve(PREALLOC_LOCI);
```

---

`165-170`: **Ploidy detection mis-handles missing genotypes**

`"./."` is split into one token (`"."`) giving ploidy = 1, not 0 or 2.  
Consider stripping missing genotypes before counting separators or fall back to the modal ploidy after the first few variants.

---

`223-227`: **Prefer `std::fill_n` over manual loop (minor)**

`std::fill(alt_counts, alt_counts + n, 0);` is already used, so the commented
manual loop can be removed to avoid confusion.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 49b1ad3fe36b20b91436b3eb6841381f1a360abb and 311508478a2e682be036f22a2a175be6bb640f66.

</details>

<details>
<summary>📒 Files selected for processing (7)</summary>

* `R/gen_tibble_vcf.R` (1 hunks)
* `R/vcf_to_fbm_cpp_fast.R` (1 hunks)
* `man/vcf_to_fbm_cpp.Rd` (1 hunks)
* `src/vcf_parser_fast.cpp` (1 hunks)
* `tests/testthat/test_gt_impute_xgboost.R` (1 hunks)
* `tests/testthat/test_gt_tibble_vcf.R` (1 hunks)
* `tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* tests/testthat/test_gt_impute_xgboost.R
* tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* R/gen_tibble_vcf.R
* R/vcf_to_fbm_cpp_fast.R

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: ubuntu-latest (oldrel-1)
* GitHub Check: ubuntu-latest (release)
* GitHub Check: pkgdown
* GitHub Check: ubuntu-latest (devel)
* GitHub Check: windows-latest (release)
* GitHub Check: test-coverage
* GitHub Check: lint
* GitHub Check: macos-latest (release)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>src/vcf_parser_fast.cpp (1)</summary>

`150-157`: **`allele1` / `allele2` are stored in the opposite order**

Most downstream code (including bigstatsr’s own helpers) expects `allele1` to be the reference (REF) and `allele2` to be the alternate (ALT).  
Here `allele1` ← `fields[4]` (ALT) and `allele2` ← `fields[3]` (REF).

Please verify the intended convention; otherwise association tests may be flipped.

```diff
-      allele1.push_back(fields[4]);   // ALT
-      allele2.push_back(fields[3]);   // REF
+      allele1.push_back(fields[3]);   // REF
+      allele2.push_back(fields[4]);   // ALT
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T05:50:15Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>tests/testthat/_test_gt_tibble_vcf.R (1)</summary><blockquote>

`1-2`: **Add documentation about non-biallelic SNPs.**

It would be helpful to include a brief comment explaining what non-biallelic SNPs are and why filtering them is important for the functionality being tested.


```diff
test_that("gen_tibble_vcf filters non-biallelic SNPs", {
+  # This test verifies that the C++ parser correctly filters out non-biallelic SNPs
+  # (variants with more than 2 alleles), as the parser is designed to work with biallelic SNPs only
  # get path for non-biallelic SNPs
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 311508478a2e682be036f22a2a175be6bb640f66 and 60eaa8c721f52ecf779ef060bcf02d89af85f394.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tests/testthat/_test_gt_tibble_vcf.R` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: ubuntu-latest (oldrel-1)
* GitHub Check: ubuntu-latest (release)
* GitHub Check: ubuntu-latest (devel)
* GitHub Check: pkgdown
* GitHub Check: windows-latest (release)
* GitHub Check: macos-latest (release)
* GitHub Check: lint
* GitHub Check: test-coverage

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T05:52:39Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>tests/testthat/test_gt_tibble_vcf.R (1)</summary><blockquote>

`1-10`: **Test setup looks good but needs additional expectations.**

The test correctly sets up a VCF file with non-biallelic SNPs and processes it with the "cpp" parser. The file path is now correct, addressing a previous review comment.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>tests/testthat/test_gt_tibble_vcf.R (1)</summary><blockquote>

`25-32`: **Good error handling test for vcfR parser.**

Testing that the vcfR parser properly errors on invalid alleles is important. However, the error message match is quite loose.

Consider using a more specific error pattern match to ensure the exact expected error is being thrown:

```diff
  expect_error(vcf_vcfr_gt <- gen_tibble(
    x = vcf_path,
    parser = "vcfR",
    backingfile = tempfile(),
    quiet = TRUE
-  ), "valid alleles are ")
+  ), "valid alleles are [ACGT]")
```

Or use regex matching with `regexp = TRUE` if the exact error message varies.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 60eaa8c721f52ecf779ef060bcf02d89af85f394 and a67fa1d021d858c247ce48039c27cce66e58c6df.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/vcf_parser_fast.cpp` (1 hunks)
* `tests/testthat/test_gt_tibble_vcf.R` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* src/vcf_parser_fast.cpp

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: ubuntu-latest (oldrel-1)
* GitHub Check: ubuntu-latest (release)
* GitHub Check: ubuntu-latest (devel)
* GitHub Check: pkgdown
* GitHub Check: windows-latest (release)
* GitHub Check: macos-latest (release)
* GitHub Check: lint
* GitHub Check: test-coverage

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>tests/testthat/test_gt_tibble_vcf.R (2)</summary>

`17-18`: **Good verification for filtering behavior.**

This expectation effectively verifies that the two non-biallelic SNPs are filtered out from the resulting tibble, which is a key behavior you want to test.

---

`19-23`: **Comprehensive comparison of filtered data.**

The approach of removing the non-biallelic SNPs from the reference dataset and comparing with the filtered result is excellent. This confirms that the filtering process works correctly and doesn't affect other data.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T07:02:04Z

Review comments:
diff_hunk: @@ -0,0 +1,346 @@
+#include <Rcpp.h>
+#include <fstream>
+#include <sstream>
+#include <vector>
+#include <string>
+#include <zlib.h>
+#include <bigstatsr/BMCodeAcc.h>
+
+using namespace Rcpp;
+using namespace std;
+
+// Function to read a line from a gzipped file
+inline bool gz_getline(gzFile file, std::string &line) {
+  char buffer[1024];
+  line.clear();
+  while (gzgets(file, buffer, sizeof(buffer)) != Z_NULL) {
+    line.append(buffer);
+    if (line.back() == '\n') {
+      line.pop_back();
+      return true;
+    }
+  }
+  return !line.empty();
+}
+
+
+// Function to split a string by multiple delimiters and return a vector of tokens
+inline std::vector<std::string> split_line(const std::string &s, const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+
+  while ((end = s.find_first_of(delimiters, start)) != std::string::npos) {
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  if (start != std::string::npos) {
+    tokens.push_back(s.substr(start));
+  }
+  return tokens;
+}
+
+// Function to split the metadata for a locus and return a vector of tokens
+// this only needs to read the first 5 elements in the string
+inline std::vector<std::string> split_locus_meta(const std::string &s,
+                                          const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+
+  for (int i = 0; i < 5; i++) {
+    end = s.find_first_of(delimiters, start);
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  return tokens;
+}
+
+
+
+/******************************************************************************
+ * Create a loci table and get ploidy and number of individuals from vcf
+ *
+ * This function parses the VCF for biallelic snps, and creates a loci table
+ * with information for those snps. It also creates a boolean vector to indicate
+ * which loci are biallelic, and a vector with the ploidy of each individual.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+// [[Rcpp::export]]
+List vcf_loci_table(std::string filename) {
+  // open the file
+  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
+  std::ifstream vcfFile;
+  gzFile vcfGzFile;
+
+  if (is_gz) {
+    vcfGzFile = gzopen(filename.c_str(), "rb");
+    if (vcfGzFile == NULL) {
+      stop("Error opening gzipped file");
+    }
+  } else {
+    vcfFile.open(filename);
+    if (!vcfFile.is_open()) {
+      stop("Error opening file");
+    }
+  }
+
+  // is a locus biallelic
+  std::vector<bool> biallelic;
+  biallelic.reserve(10e6);
+  // loci metadata (preallocate a million loci, as a start)
+  std::vector<std::string>chromosome;
+  chromosome.reserve(10e6);
+  std::vector<std::string> marker_id;
+  marker_id.reserve(10e6);
+  std::vector<int32_t> physical_pos;
+  physical_pos.reserve(10e6);
+  std::vector<std::string> allele1;
+  allele1.reserve(10e6);
+  std::vector<std::string> allele2;
+  allele2.reserve(10e6);
+  // individual metadata
+  std::vector<int> ploidy;
+  std::vector<std::string> sample_names;
+
+
+  std::string line;
+  std::string line_old; // used to get the samples from the last line of the header
+  int n_header_lines =0;
+
+  bool get_sample_names = true;
+  bool get_ploidy = true;
+
+  // Read the file line by line until the end of file
+  while (true) {
+    bool gotLine;
+    if (is_gz) {
+      gotLine = gz_getline(vcfGzFile, line);
+    } else {
+      gotLine = (bool)std::getline(vcfFile, line);
+    }
+
+    // Check if we reached the end of the file
+    if (!gotLine) {
+      break;
+    }
+
+    // Skip header lines
+    if (line[0] == '#') {
+      line_old = line; // save the last line of the header
+      n_header_lines++;
+      continue;
+    }
+
+    // Check if we need to get the sample names (this is only the case for
+    // the first line after headers)
+    if (get_sample_names) {
+      std::vector<std::string> fields = split_line(line_old, "\t");
+     for (unsigned int i = 9; i < fields.size(); ++i) {
+        sample_names.push_back(fields[i]);
+      }
+      get_sample_names = false;
+    }
+
+    // Extract chromosome, position, and
+    // Split the VCF line into fields (only for the first few fields)
+    std::vector<std::string> fields = split_locus_meta(line, "\t");
+
+    // Check if the marker is biallelic SNP (REF and ALT allele are single character)
+    // if yes, get the metadata for this locus
+    if ((fields[3].length() ==1) && (fields[4].length() ==1)) {
+      chromosome.push_back(fields[0]);
+      physical_pos.push_back(std::stoi(fields[1]));
+      marker_id.push_back(fields[2]);
+      allele1.push_back(fields[4]);
+      allele2.push_back(fields[3]);
+    }
+
+
+    // Check if we need to get ploidy (this is only the case for the first line
+    // of real data)
+    if (get_ploidy) {
+      std::vector<std::string> fields = split_line(line, "\t");
+      for (unsigned int i = 9; i < fields.size(); ++i) {
+        ploidy.push_back(split_line(fields[i], "/|").size());
+      }
+      get_ploidy = false;
+    }
+
+  }
+
+  // close the file
+  if (is_gz) {
+    gzclose(vcfGzFile);
+  } else {
+    vcfFile.close();
+  }
+
+  // craete loci table
+  DataFrame loci_table = DataFrame::create( Named("chromosome") = chromosome ,
+                                            _["marker.ID"] = marker_id,
+                                            _["physical.pos"] = physical_pos,
+                                            _["allele1"] = allele1,
+                                            _["allele2"] = allele2);
+
+
+  // Return a list containing the matrix and the ploidy vector
+  return (List::create(Named("loci_tbl") = loci_table,
+                       Named("sample_names") = sample_names,
+                       Named("ploidy") = ploidy,
+                       Named("biallelic") = biallelic,
+                       Named("n_header_lines") = n_header_lines));
+}
+
+
+// Function to count the number of alternate alleles from genotype information
+//
+/******************************************************************************
+ * count the number of alternate alleles from genotype information
+ *
+ * missingValue is max_ploidy +1
+ *
+ * @param filename The name of the VCF file to parse
+ * This function parses a line of the VCF and counts the number of alternate alleles,
+ * storing it in an array of raw values (than can then be written directly to the
+ * file backed matrix).
+ *
+ * @param string line The line of the VCF file to parse
+ * @param unsigned char* alt_counts The array to store the counts
+ * @param int n The number of individuals
+ * @param int missingValue The value to use for missing genotypes
+ */
+
+inline void count_alt_alleles(const std::string &line,
+                          unsigned char* alt_counts,
+                          size_t* separator_pos,
+                          size_t n,
+                          const size_t missingValue,
+                          const std::string &separator
+                          ) {
+  // reset counts to 0
+  for (uint i = 0; i < n; i++) {
+    alt_counts[i] = 0;
+  }
+
+  separator_pos[0] = line.find(separator, 0);
+  // find the position of all separators
+  for (size_t i = 1; i < 8+n; i++) {
+    separator_pos[i] = line.find(separator, separator_pos[i-1]+1);
+  }
+  separator_pos[8+n] = line.length();
+
+  // now move through each of the genotypes and process them
+  // we ignore the first 9 fields (so start from position 9)
+  for (size_t i=0; i<n; i++){
+    size_t pos = separator_pos[i+8];
+    // the genotypes start at pos +1
+    if (line[pos+1] == '.') {
+      alt_counts[i] = missingValue;
+    } else {
+      for (size_t pos_i = pos+1; pos_i < separator_pos[i+9]+1; pos_i++){
+        if (line[pos_i] == '1'){
+          alt_counts[i]++;
+        } else if (line[pos_i] == ':') //if at the end of the genotype section
+          break;
+      }
+    }
+  }
+}
+
+
+
+
+
+/******************************************************************************
+ * Read the genotypes into a File Backed Matrix from a vcf
+ *
+ * This function parses the VCF, using information gathered from vcf_loci_table.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+
+// [[Rcpp::export]]
+bool vcf_genotypes_to_fbm(std::string filename,
+                                   Environment BM,
+                                   IntegerVector& biallelic,
+                                   const int missing_value,
+                                   const int n_header_lines) {
path: src/vcf_parser_fast.cpp
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Unused parameter `biallelic`**

`vcf_genotypes_to_fbm()` receives `IntegerVector& biallelic` but never references it, reinforcing the desynchronisation problem and wasting memory copies.  
After addressing the skipping logic above, either remove the parameter or put it to use.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 3115084
created_at: 2025-05-09T16:09:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,346 @@
+#include <Rcpp.h>
+#include <fstream>
+#include <sstream>
+#include <vector>
+#include <string>
+#include <zlib.h>
+#include <bigstatsr/BMCodeAcc.h>
+
+using namespace Rcpp;
+using namespace std;
+
+// Function to read a line from a gzipped file
+inline bool gz_getline(gzFile file, std::string &line) {
+  char buffer[1024];
+  line.clear();
+  while (gzgets(file, buffer, sizeof(buffer)) != Z_NULL) {
+    line.append(buffer);
+    if (line.back() == '\n') {
+      line.pop_back();
+      return true;
+    }
+  }
+  return !line.empty();
+}
+
+
+// Function to split a string by multiple delimiters and return a vector of tokens
+inline std::vector<std::string> split_line(const std::string &s, const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+
+  while ((end = s.find_first_of(delimiters, start)) != std::string::npos) {
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  if (start != std::string::npos) {
+    tokens.push_back(s.substr(start));
+  }
+  return tokens;
+}
+
+// Function to split the metadata for a locus and return a vector of tokens
+// this only needs to read the first 5 elements in the string
+inline std::vector<std::string> split_locus_meta(const std::string &s,
+                                          const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+
+  for (int i = 0; i < 5; i++) {
+    end = s.find_first_of(delimiters, start);
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  return tokens;
+}
+
+
+
+/******************************************************************************
+ * Create a loci table and get ploidy and number of individuals from vcf
+ *
+ * This function parses the VCF for biallelic snps, and creates a loci table
+ * with information for those snps. It also creates a boolean vector to indicate
+ * which loci are biallelic, and a vector with the ploidy of each individual.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+// [[Rcpp::export]]
+List vcf_loci_table(std::string filename) {
+  // open the file
+  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
+  std::ifstream vcfFile;
+  gzFile vcfGzFile;
+
+  if (is_gz) {
+    vcfGzFile = gzopen(filename.c_str(), "rb");
+    if (vcfGzFile == NULL) {
+      stop("Error opening gzipped file");
+    }
+  } else {
+    vcfFile.open(filename);
+    if (!vcfFile.is_open()) {
+      stop("Error opening file");
+    }
+  }
+
+  // is a locus biallelic
+  std::vector<bool> biallelic;
+  biallelic.reserve(10e6);
+  // loci metadata (preallocate a million loci, as a start)
+  std::vector<std::string>chromosome;
+  chromosome.reserve(10e6);
+  std::vector<std::string> marker_id;
+  marker_id.reserve(10e6);
+  std::vector<int32_t> physical_pos;
+  physical_pos.reserve(10e6);
+  std::vector<std::string> allele1;
+  allele1.reserve(10e6);
+  std::vector<std::string> allele2;
+  allele2.reserve(10e6);
+  // individual metadata
+  std::vector<int> ploidy;
+  std::vector<std::string> sample_names;
+
+
+  std::string line;
+  std::string line_old; // used to get the samples from the last line of the header
+  int n_header_lines =0;
+
+  bool get_sample_names = true;
+  bool get_ploidy = true;
+
+  // Read the file line by line until the end of file
+  while (true) {
+    bool gotLine;
+    if (is_gz) {
+      gotLine = gz_getline(vcfGzFile, line);
+    } else {
+      gotLine = (bool)std::getline(vcfFile, line);
+    }
+
+    // Check if we reached the end of the file
+    if (!gotLine) {
+      break;
+    }
+
+    // Skip header lines
+    if (line[0] == '#') {
+      line_old = line; // save the last line of the header
+      n_header_lines++;
+      continue;
+    }
+
+    // Check if we need to get the sample names (this is only the case for
+    // the first line after headers)
+    if (get_sample_names) {
+      std::vector<std::string> fields = split_line(line_old, "\t");
+     for (unsigned int i = 9; i < fields.size(); ++i) {
+        sample_names.push_back(fields[i]);
+      }
+      get_sample_names = false;
+    }
+
+    // Extract chromosome, position, and
+    // Split the VCF line into fields (only for the first few fields)
+    std::vector<std::string> fields = split_locus_meta(line, "\t");
+
+    // Check if the marker is biallelic SNP (REF and ALT allele are single character)
+    // if yes, get the metadata for this locus
+    if ((fields[3].length() ==1) && (fields[4].length() ==1)) {
+      chromosome.push_back(fields[0]);
+      physical_pos.push_back(std::stoi(fields[1]));
+      marker_id.push_back(fields[2]);
+      allele1.push_back(fields[4]);
+      allele2.push_back(fields[3]);
+    }
path: src/vcf_parser_fast.cpp
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Mismatch between loci table and genotype matrix**

`vcf_loci_table()` records only loci where `REF` and `ALT` are single bases, yet  
`vcf_genotypes_to_fbm()` blindly iterates over *every* subsequent line, assuming the same number of loci (`n_loci`).  
If the VCF contains non-biallelic sites, the genotype matrix will receive rows that do **not** correspond to the returned metadata, and the loop will terminate early, leaving the FBM half-filled.

Fix strategies  
1. Keep a parallel `biallelic` mask (see previous comment) and, in the genotype phase, **skip lines** where `biallelic[i] == false`.  
2. Alternatively, filter the VCF twice (or stream once and store offsets) so both steps process identical loci counts.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 3115084
created_at: 2025-05-09T16:09:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,346 @@
+#include <Rcpp.h>
+#include <fstream>
+#include <sstream>
+#include <vector>
+#include <string>
+#include <zlib.h>
+#include <bigstatsr/BMCodeAcc.h>
+
+using namespace Rcpp;
+using namespace std;
+
+// Function to read a line from a gzipped file
+inline bool gz_getline(gzFile file, std::string &line) {
+  char buffer[1024];
+  line.clear();
+  while (gzgets(file, buffer, sizeof(buffer)) != Z_NULL) {
+    line.append(buffer);
+    if (line.back() == '\n') {
+      line.pop_back();
+      return true;
+    }
+  }
+  return !line.empty();
+}
+
+
+// Function to split a string by multiple delimiters and return a vector of tokens
+inline std::vector<std::string> split_line(const std::string &s, const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+
+  while ((end = s.find_first_of(delimiters, start)) != std::string::npos) {
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  if (start != std::string::npos) {
+    tokens.push_back(s.substr(start));
+  }
+  return tokens;
+}
+
+// Function to split the metadata for a locus and return a vector of tokens
+// this only needs to read the first 5 elements in the string
+inline std::vector<std::string> split_locus_meta(const std::string &s,
+                                          const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+
+  for (int i = 0; i < 5; i++) {
+    end = s.find_first_of(delimiters, start);
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  return tokens;
+}
+
+
+
+/******************************************************************************
+ * Create a loci table and get ploidy and number of individuals from vcf
+ *
+ * This function parses the VCF for biallelic snps, and creates a loci table
+ * with information for those snps. It also creates a boolean vector to indicate
+ * which loci are biallelic, and a vector with the ploidy of each individual.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+// [[Rcpp::export]]
+List vcf_loci_table(std::string filename) {
+  // open the file
+  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
+  std::ifstream vcfFile;
+  gzFile vcfGzFile;
+
+  if (is_gz) {
+    vcfGzFile = gzopen(filename.c_str(), "rb");
+    if (vcfGzFile == NULL) {
+      stop("Error opening gzipped file");
+    }
+  } else {
+    vcfFile.open(filename);
+    if (!vcfFile.is_open()) {
+      stop("Error opening file");
+    }
+  }
+
+  // is a locus biallelic
+  std::vector<bool> biallelic;
+  biallelic.reserve(10e6);
+  // loci metadata (preallocate a million loci, as a start)
+  std::vector<std::string>chromosome;
+  chromosome.reserve(10e6);
+  std::vector<std::string> marker_id;
+  marker_id.reserve(10e6);
+  std::vector<int32_t> physical_pos;
+  physical_pos.reserve(10e6);
+  std::vector<std::string> allele1;
+  allele1.reserve(10e6);
+  std::vector<std::string> allele2;
+  allele2.reserve(10e6);
path: src/vcf_parser_fast.cpp
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`biallelic` vector is never filled**

You reserve memory for `biallelic` but never push values into it.  
Down-stream R code expects `length(biallelic) == nrow(loci_tbl)` and passes it to `vcf_genotypes_to_fbm()`.  
Returning a zero-length vector silently breaks this contract and can desynchronise locus selection.

```diff
-  // is a locus biallelic
-  std::vector<bool> biallelic;
+  // is a locus biallelic (filled in parallel with loci metadata)
+  std::vector<bool> biallelic;
```

Then, inside the main loop, push the flag for every VCF line (even those you finally skip) so the two files stay consistent.




> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 3115084
created_at: 2025-05-09T16:09:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,346 @@
+#include <Rcpp.h>
+#include <fstream>
+#include <sstream>
+#include <vector>
+#include <string>
+#include <zlib.h>
+#include <bigstatsr/BMCodeAcc.h>
+
+using namespace Rcpp;
+using namespace std;
+
+// Function to read a line from a gzipped file
+inline bool gz_getline(gzFile file, std::string &line) {
+  char buffer[1024];
+  line.clear();
+  while (gzgets(file, buffer, sizeof(buffer)) != Z_NULL) {
+    line.append(buffer);
+    if (line.back() == '\n') {
+      line.pop_back();
+      return true;
+    }
+  }
+  return !line.empty();
+}
+
+
+// Function to split a string by multiple delimiters and return a vector of tokens
+inline std::vector<std::string> split_line(const std::string &s, const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+
+  while ((end = s.find_first_of(delimiters, start)) != std::string::npos) {
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  if (start != std::string::npos) {
+    tokens.push_back(s.substr(start));
+  }
+  return tokens;
+}
+
+// Function to split the metadata for a locus and return a vector of tokens
+// this only needs to read the first 5 elements in the string
+inline std::vector<std::string> split_locus_meta(const std::string &s,
+                                          const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+
+  for (int i = 0; i < 5; i++) {
+    end = s.find_first_of(delimiters, start);
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  return tokens;
+}
+
+
+
+/******************************************************************************
+ * Create a loci table and get ploidy and number of individuals from vcf
+ *
+ * This function parses the VCF for biallelic snps, and creates a loci table
+ * with information for those snps. It also creates a boolean vector to indicate
+ * which loci are biallelic, and a vector with the ploidy of each individual.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+// [[Rcpp::export]]
+List vcf_loci_table(std::string filename) {
+  // open the file
+  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
+  std::ifstream vcfFile;
+  gzFile vcfGzFile;
+
+  if (is_gz) {
+    vcfGzFile = gzopen(filename.c_str(), "rb");
+    if (vcfGzFile == NULL) {
+      stop("Error opening gzipped file");
+    }
+  } else {
+    vcfFile.open(filename);
+    if (!vcfFile.is_open()) {
+      stop("Error opening file");
+    }
+  }
+
+  // is a locus biallelic
+  std::vector<bool> biallelic;
+  biallelic.reserve(10e6);
+  // loci metadata (preallocate a million loci, as a start)
+  std::vector<std::string>chromosome;
+  chromosome.reserve(10e6);
+  std::vector<std::string> marker_id;
+  marker_id.reserve(10e6);
+  std::vector<int32_t> physical_pos;
+  physical_pos.reserve(10e6);
+  std::vector<std::string> allele1;
+  allele1.reserve(10e6);
+  std::vector<std::string> allele2;
+  allele2.reserve(10e6);
+  // individual metadata
+  std::vector<int> ploidy;
+  std::vector<std::string> sample_names;
+
+
+  std::string line;
+  std::string line_old; // used to get the samples from the last line of the header
+  int n_header_lines =0;
+
+  bool get_sample_names = true;
+  bool get_ploidy = true;
+
+  // Read the file line by line until the end of file
+  while (true) {
+    bool gotLine;
+    if (is_gz) {
+      gotLine = gz_getline(vcfGzFile, line);
+    } else {
+      gotLine = (bool)std::getline(vcfFile, line);
+    }
+
+    // Check if we reached the end of the file
+    if (!gotLine) {
+      break;
+    }
+
+    // Skip header lines
+    if (line[0] == '#') {
+      line_old = line; // save the last line of the header
+      n_header_lines++;
+      continue;
+    }
+
+    // Check if we need to get the sample names (this is only the case for
+    // the first line after headers)
+    if (get_sample_names) {
+      std::vector<std::string> fields = split_line(line_old, "\t");
+     for (unsigned int i = 9; i < fields.size(); ++i) {
+        sample_names.push_back(fields[i]);
+      }
+      get_sample_names = false;
+    }
+
+    // Extract chromosome, position, and
+    // Split the VCF line into fields (only for the first few fields)
+    std::vector<std::string> fields = split_locus_meta(line, "\t");
+
+    // Check if the marker is biallelic SNP (REF and ALT allele are single character)
+    // if yes, get the metadata for this locus
+    if ((fields[3].length() ==1) && (fields[4].length() ==1)) {
+      chromosome.push_back(fields[0]);
+      physical_pos.push_back(std::stoi(fields[1]));
+      marker_id.push_back(fields[2]);
+      allele1.push_back(fields[4]);
+      allele2.push_back(fields[3]);
+    }
+
+
+    // Check if we need to get ploidy (this is only the case for the first line
+    // of real data)
+    if (get_ploidy) {
+      std::vector<std::string> fields = split_line(line, "\t");
+      for (unsigned int i = 9; i < fields.size(); ++i) {
+        ploidy.push_back(split_line(fields[i], "/|").size());
+      }
+      get_ploidy = false;
+    }
+
+  }
+
+  // close the file
+  if (is_gz) {
+    gzclose(vcfGzFile);
+  } else {
+    vcfFile.close();
+  }
+
+  // craete loci table
+  DataFrame loci_table = DataFrame::create( Named("chromosome") = chromosome ,
+                                            _["marker.ID"] = marker_id,
+                                            _["physical.pos"] = physical_pos,
+                                            _["allele1"] = allele1,
+                                            _["allele2"] = allele2);
+
+
+  // Return a list containing the matrix and the ploidy vector
+  return (List::create(Named("loci_tbl") = loci_table,
+                       Named("sample_names") = sample_names,
+                       Named("ploidy") = ploidy,
+                       Named("biallelic") = biallelic,
+                       Named("n_header_lines") = n_header_lines));
+}
+
+
+// Function to count the number of alternate alleles from genotype information
+//
+/******************************************************************************
+ * count the number of alternate alleles from genotype information
+ *
+ * missingValue is max_ploidy +1
+ *
+ * @param filename The name of the VCF file to parse
+ * This function parses a line of the VCF and counts the number of alternate alleles,
+ * storing it in an array of raw values (than can then be written directly to the
+ * file backed matrix).
+ *
+ * @param string line The line of the VCF file to parse
+ * @param unsigned char* alt_counts The array to store the counts
+ * @param int n The number of individuals
+ * @param int missingValue The value to use for missing genotypes
+ */
+
+inline void count_alt_alleles(const std::string &line,
+                          unsigned char* alt_counts,
+                          size_t* separator_pos,
+                          size_t n,
+                          const size_t missingValue,
+                          const std::string &separator
+                          ) {
+  // reset counts to 0
+  for (uint i = 0; i < n; i++) {
+    alt_counts[i] = 0;
+  }
+
+  separator_pos[0] = line.find(separator, 0);
+  // find the position of all separators
+  for (size_t i = 1; i < 8+n; i++) {
+    separator_pos[i] = line.find(separator, separator_pos[i-1]+1);
+  }
+  separator_pos[8+n] = line.length();
+
+  // now move through each of the genotypes and process them
+  // we ignore the first 9 fields (so start from position 9)
+  for (size_t i=0; i<n; i++){
+    size_t pos = separator_pos[i+8];
+    // the genotypes start at pos +1
+    if (line[pos+1] == '.') {
+      alt_counts[i] = missingValue;
+    } else {
+      for (size_t pos_i = pos+1; pos_i < separator_pos[i+9]+1; pos_i++){
+        if (line[pos_i] == '1'){
+          alt_counts[i]++;
+        } else if (line[pos_i] == ':') //if at the end of the genotype section
+          break;
+      }
+    }
+  }
+}
+
+
+
+
+
+/******************************************************************************
+ * Read the genotypes into a File Backed Matrix from a vcf
+ *
+ * This function parses the VCF, using information gathered from vcf_loci_table.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+
+// [[Rcpp::export]]
+bool vcf_genotypes_to_fbm(std::string filename,
+                                   Environment BM,
+                                   IntegerVector& biallelic,
+                                   const int missing_value,
+                                   const int n_header_lines) {
+  // open the file
+  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
+  std::ifstream vcfFile;
+  gzFile vcfGzFile;
+
+  if (is_gz) {
+    vcfGzFile = gzopen(filename.c_str(), "rb");
+    if (vcfGzFile == NULL) {
+      stop("Error opening gzipped file");
+    }
+  } else {
+    vcfFile.open(filename);
+    if (!vcfFile.is_open()) {
+      stop("Error opening file");
+    }
+  }
+
+  // set up access to the file backed matrix
+  XPtr<FBM_RW> xpBM = BM["address_rw"];
+  unsigned char* ptr = static_cast<unsigned char*>(xpBM->matrix());
+//  const unsigned char* code_ptr;
+  int n = xpBM->nrow(); // number of individuals
+  int n_loci = xpBM->ncol(); // number of loci
+
+  // string to store the line read from the file
+  std::string line;
+  std::string tab_delimiter = "\t";
+
+  // Skip the header
+  for (int i = 0; i < n_header_lines; i++) {
+    if (is_gz) {
+      gz_getline(vcfGzFile, line);
+    } else {
+      std::getline(vcfFile, line);
+    }
+  }
+  // array to store the genotypes
path: src/vcf_parser_fast.cpp
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Leak: `separator_pos` is never freed**

The heap-allocated array `separator_pos` (`new size_t[n+9]`) is not deleted, causing a leak for every VCF import.

```diff
-  // delete the array
-  delete[] alt_counts;
+  delete[] alt_counts;
+  delete[] separator_pos;
```



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 43995bc
created_at: 2025-05-09T16:09:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+test_that("gen_tibble_vcf filters non-biallelic SNPs", {
+  # get path for non-biallelic SNPs
+  vcf_path <- test_path("testdata/pop_a_non-biallelic.vcf")
+  # read in the vcf
+  vcf_cpp_gt <- gen_tibble_vcf(
+    x = vcf_path,
+    parser = "cpp",
+    backingfile = tempfile(),
+    quiet = TRUE
+  )
+  
+})
path: tests/testthat/test_gt_tibble_vcf.R
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**The test has no expectations – it never asserts anything**

Calling the function without at least one `expect_*()` still produces a *passing* test but contributes nothing to coverage and may hide silent failures.

Add-on ideas (choose at least one):
• `expect_s3_class(vcf_cpp_gt, "bigSNP")`  
• `expect_true(nrow(vcf_cpp_gt$map) > 0)`  
• `expect_false(any(vcf_cpp_gt$biallelic == FALSE))`  

This will actively verify that non-biallelic loci are filtered.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit a67fa1d
created_at: 2025-05-10T05:50:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+test_that("gen_tibble_vcf filters non-biallelic SNPs", {
+  # get path for non-biallelic SNPs
+  vcf_path <- test_path("testdata/pop_a_non-biallelic.vcf")
path: tests/testthat/test_gt_tibble_vcf.R
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Incorrect test-data path will make the test fail on CI**

`test_path("testdata/pop_a_non-biallelic.vcf")` differs from the file that was actually added (`tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf`, underscore + extra `vcf/` sub-dir).  
The test will error with “file does not exist” once executed in a clean checkout.

```diff
-  vcf_path <- test_path("testdata/pop_a_non-biallelic.vcf")
+  vcf_path <- test_path("testdata/vcf/pop_a_non_biallelic.vcf")
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  vcf_path <- test_path("testdata/vcf/pop_a_non-biallelic.vcf")
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit a67fa1d
created_at: 2025-05-10T05:50:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,351 @@
+#include <Rcpp.h>
+#include <fstream>
+#include <sstream>
+#include <vector>
+#include <string>
+#include <zlib.h>
+#include <bigstatsr/BMCodeAcc.h>
+
+using namespace Rcpp;
+using namespace std;
+
+// Function to read a line from a gzipped file
+inline bool gz_getline(gzFile file, std::string &line) {
+  char buffer[1024];
+  line.clear();
+  while (gzgets(file, buffer, sizeof(buffer)) != Z_NULL) {
+    line.append(buffer);
+    if (line.back() == '\n') {
+      line.pop_back();
+      return true;
+    }
+  }
+  return !line.empty();
+}
+
+
+// Function to split a string by multiple delimiters and return a vector of tokens
+inline std::vector<std::string> split_line(const std::string &s, const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+  
+  while ((end = s.find_first_of(delimiters, start)) != std::string::npos) {
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  if (start != std::string::npos) {
+    tokens.push_back(s.substr(start));
+  }
+  return tokens;
+}
+
+// Function to split the metadata for a locus and return a vector of tokens
+// this only needs to read the first 5 elements in the string
+inline std::vector<std::string> split_locus_meta(const std::string &s,
+                                                 const std::string &delimiters) {
+  std::vector<std::string> tokens;
+  size_t start = s.find_first_not_of(delimiters), end = 0;
+  
+  for (int i = 0; i < 5; i++) {
+    end = s.find_first_of(delimiters, start);
+    tokens.push_back(s.substr(start, end - start));
+    start = s.find_first_not_of(delimiters, end);
+  }
+  return tokens;
+}
+
+
+
+/******************************************************************************
+ * Create a loci table and get ploidy and number of individuals from vcf
+ *
+ * This function parses the VCF for biallelic snps, and creates a loci table
+ * with information for those snps. It also creates a boolean vector to indicate
+ * which loci are biallelic, and a vector with the ploidy of each individual.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+// [[Rcpp::export]]
+List vcf_loci_table(std::string filename) {
+  // open the file
+  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
+  std::ifstream vcfFile;
+  gzFile vcfGzFile;
+  
+  if (is_gz) {
+    vcfGzFile = gzopen(filename.c_str(), "rb");
+    if (vcfGzFile == NULL) {
+      stop("Error opening gzipped file");
+    }
+  } else {
+    vcfFile.open(filename);
+    if (!vcfFile.is_open()) {
+      stop("Error opening file");
+    }
+  }
+  
+  // is a locus biallelic
+  std::vector<bool> biallelic;
+  biallelic.reserve(10e6);
+  // loci metadata (preallocate a million loci, as a start)
+  std::vector<std::string>chromosome;
+  chromosome.reserve(10e6);
+  std::vector<std::string> marker_id;
+  marker_id.reserve(10e6);
+  std::vector<int32_t> physical_pos;
+  physical_pos.reserve(10e6);
+  std::vector<std::string> allele1;
+  allele1.reserve(10e6);
+  std::vector<std::string> allele2;
+  allele2.reserve(10e6);
+  // individual metadata
+  std::vector<int> ploidy;
+  std::vector<std::string> sample_names;
+  
+  
+  std::string line;
+  std::string line_old; // used to get the samples from the last line of the header
+  int n_header_lines =0;
+  
+  bool get_sample_names = true;
+  bool get_ploidy = true;
+  
+  // Read the file line by line until the end of file
+  while (true) {
+    bool gotLine;
+    if (is_gz) {
+      gotLine = gz_getline(vcfGzFile, line);
+    } else {
+      gotLine = (bool)std::getline(vcfFile, line);
+    }
+    
+    // Check if we reached the end of the file
+    if (!gotLine) {
+      break;
+    }
+    
+    // Skip header lines
+    if (line[0] == '#') {
+      line_old = line; // save the last line of the header
+      n_header_lines++;
+      continue;
+    }
+    
+    // Check if we need to get the sample names (this is only the case for
+    // the first line after headers)
+    if (get_sample_names) {
+      std::vector<std::string> fields = split_line(line_old, "\t");
+      for (unsigned int i = 9; i < fields.size(); ++i) {
+        sample_names.push_back(fields[i]);
+      }
+      get_sample_names = false;
+    }
+    
+    // Extract chromosome, position, and
+    // Split the VCF line into fields (only for the first few fields)
+    std::vector<std::string> fields = split_locus_meta(line, "\t");
+    
+    // Check if the marker is biallelic SNP (REF and ALT allele are single character)
+    // if yes, get the metadata for this locus
+    if ((fields[3].length() ==1) && (fields[4].length() ==1)) {
+      biallelic.push_back(true);
+      chromosome.push_back(fields[0]);
+      physical_pos.push_back(std::stoi(fields[1]));
+      marker_id.push_back(fields[2]);
+      allele1.push_back(fields[4]);
+      allele2.push_back(fields[3]);
+    } else {
+      biallelic.push_back(false);
+    }
+    
+    
+    // Check if we need to get ploidy (this is only the case for the first line
+    // of real data)
+    if (get_ploidy) {
+      std::vector<std::string> fields = split_line(line, "\t");
+      for (unsigned int i = 9; i < fields.size(); ++i) {
+        ploidy.push_back(split_line(fields[i], "/|").size());
+      }
+      get_ploidy = false;
+    }
+    
+  }
+  
+  // close the file
+  if (is_gz) {
+    gzclose(vcfGzFile);
+  } else {
+    vcfFile.close();
+  }
+  
+  // craete loci table
+  DataFrame loci_table = DataFrame::create( Named("chromosome") = chromosome ,
+                                            _["marker.ID"] = marker_id,
+                                            _["physical.pos"] = physical_pos,
+                                            _["allele1"] = allele1,
+                                            _["allele2"] = allele2);
+  
+  
+  // Return a list containing the matrix and the ploidy vector
+  return (List::create(Named("loci_tbl") = loci_table,
+                       Named("sample_names") = sample_names,
+                       Named("ploidy") = ploidy,
+                       Named("biallelic") = biallelic,
+                       Named("n_header_lines") = n_header_lines));
+}
+
+
+// Function to count the number of alternate alleles from genotype information
+//
+/******************************************************************************
+ * count the number of alternate alleles from genotype information
+ *
+ * missingValue is max_ploidy +1
+ *
+ * @param filename The name of the VCF file to parse
+ * This function parses a line of the VCF and counts the number of alternate alleles,
+ * storing it in an array of raw values (than can then be written directly to the
+ * file backed matrix).
+ *
+ * @param string line The line of the VCF file to parse
+ * @param unsigned char* alt_counts The array to store the counts
+ * @param int n The number of individuals
+ * @param int missingValue The value to use for missing genotypes
+ */
+
+inline void count_alt_alleles(const std::string &line,
+                              unsigned char* alt_counts,
+                              size_t* separator_pos,
+                              size_t n,
+                              const size_t missingValue,
+                              const std::string &separator
+) {
+  // reset counts to 0
+  std::fill(alt_counts, alt_counts+n, 0);
+  // for (uint i = 0; i < n; i++) {
+  //   alt_counts[i] = 0;
+  // }
+  
+  separator_pos[0] = line.find(separator, 0);
+  // find the position of all separators
+  for (size_t i = 1; i < 8+n; i++) {
+    separator_pos[i] = line.find(separator, separator_pos[i-1]+1);
+  }
+  separator_pos[8+n] = line.length();
+  
+  // now move through each of the genotypes and process them
+  // we ignore the first 9 fields (so start from position 9)
+  for (size_t i=0; i<n; i++){
+    size_t pos = separator_pos[i+8];
+    // the genotypes start at pos +1
+    if (line[pos+1] == '.') {
+      alt_counts[i] = missingValue;
+    } else {
+      for (size_t pos_i = pos+1; pos_i < separator_pos[i+9]+1; pos_i++){
+        if (line[pos_i] == '1'){
+          alt_counts[i]++;
+        } else if (line[pos_i] == ':') //if at the end of the genotype section
+          break;
+      }
+    }
+  }
+}
+
+
+
+
+
+/******************************************************************************
+ * Read the genotypes into a File Backed Matrix from a vcf
+ *
+ * This function parses the VCF, using information gathered from vcf_loci_table.
+ *
+ * @param filename The name of the VCF file to parse
+ */
+
+// [[Rcpp::export]]
+bool vcf_genotypes_to_fbm(std::string filename,
+                          Environment BM,
+                          IntegerVector& biallelic,
+                          const int missing_value,
+                          const int n_header_lines) {
+  // open the file
+  bool is_gz = (filename.substr(filename.size() - 3) == ".gz");
+  std::ifstream vcfFile;
+  gzFile vcfGzFile;
+  
+  if (is_gz) {
+    vcfGzFile = gzopen(filename.c_str(), "rb");
+    if (vcfGzFile == NULL) {
+      stop("Error opening gzipped file");
+    }
+  } else {
+    vcfFile.open(filename);
+    if (!vcfFile.is_open()) {
+      stop("Error opening file");
+    }
+  }
+  
+  // set up access to the file backed matrix
+  XPtr<FBM_RW> xpBM = BM["address_rw"];
+  unsigned char* ptr = static_cast<unsigned char*>(xpBM->matrix());
+  //  const unsigned char* code_ptr;
+  int n = xpBM->nrow(); // number of individuals
+  int n_loci = xpBM->ncol(); // number of loci
+  
+  // string to store the line read from the file
+  std::string line;
+  std::string tab_delimiter = "\t";
+  
+  // Skip the header
+  for (int i = 0; i < n_header_lines; i++) {
+    if (is_gz) {
+      gz_getline(vcfGzFile, line);
+    } else {
+      std::getline(vcfFile, line);
+    }
+  }
+  // array to store the genotypes
+  unsigned char* alt_counts = new unsigned char[n];
+  // position of the separator, the last value is the end of the string
+  size_t* separator_pos = new size_t[n+9];
+  
path: src/vcf_parser_fast.cpp
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Memory leak – `separator_pos` is never freed**

`separator_pos` is allocated with `new size_t[...]` but not deleted, leaking ~`(n+9)*sizeof(size_t)` bytes per import.

```diff
-  // delete the array
-  delete[] alt_counts;
+  delete[] alt_counts;
+  delete[] separator_pos;
```



Also applies to: 346-349

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 43995bc
created_at: 2025-05-10T05:50:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+test_that("gen_tibble_vcf filters non-biallelic SNPs", {
+  # get path for non-biallelic SNPs
+  vcf_path <- test_path("testdata/pop_a_non_biallelic.vcf")
+  # read in the vcf
+  vcf_cpp_gt <- gen_tibble(
+    x = vcf_path,
+    parser = "cpp",
+    backingfile = tempfile(),
+    quiet = TRUE
+  )
+  
+})
path: tests/testthat/_test_gt_tibble_vcf.R
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Test is incomplete - missing assertions to verify filtering behavior.**

While the test creates a tibble from a VCF file with non-biallelic SNPs, it doesn't include any assertions to verify that the filtering actually works. The test will always pass in its current form, regardless of whether the filtering is functioning correctly.


Add assertions to verify filtering behavior. For example:

```diff
test_that("gen_tibble_vcf filters non-biallelic SNPs", {
  # get path for non-biallelic SNPs
  vcf_path <- test_path("testdata/pop_a_non_biallelic.vcf")
  # read in the vcf
  vcf_cpp_gt <- gen_tibble(
    x = vcf_path,
    parser = "cpp",
    backingfile = tempfile(),
    quiet = TRUE
  )
  
+ # Verify non-biallelic SNPs were filtered
+ expect_true(all(vcf_cpp_gt$n_alleles == 2), "Non-biallelic SNPs weren't filtered properly")
+ # Or check that specific variants were properly excluded
+ expect_false(any(vcf_cpp_gt$pos %in% c(...)), "Known non-biallelic positions weren't filtered")
})
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
test_that("gen_tibble_vcf filters non-biallelic SNPs", {
  # get path for non-biallelic SNPs
  vcf_path <- test_path("testdata/pop_a_non_biallelic.vcf")
  # read in the vcf
  vcf_cpp_gt <- gen_tibble(
    x = vcf_path,
    parser = "cpp",
    backingfile = tempfile(),
    quiet = TRUE
  )
  
  # Verify non-biallelic SNPs were filtered
  expect_true(all(vcf_cpp_gt$n_alleles == 2), "Non-biallelic SNPs weren't filtered properly")
  # Or check that specific variants were properly excluded
  expect_false(any(vcf_cpp_gt$pos %in% c(...)), "Known non-biallelic positions weren't filtered")
})
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T05:52:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,12 @@
+test_that("gen_tibble_vcf filters non-biallelic SNPs", {
+  # get path for non-biallelic SNPs
+  vcf_path <- test_path("testdata/pop_a_non_biallelic.vcf")
path: tests/testthat/_test_gt_tibble_vcf.R
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify the file path is correct.**

The path `test_path("testdata/pop_a_non_biallelic.vcf")` may be incorrect. According to the summary, the test data should be at `tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf` (note the additional "vcf/" directory).

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the file exists at both potential locations
echo "Checking file paths..."
ls -l tests/testthat/testdata/pop_a_non_biallelic.vcf 2>/dev/null || echo "File not found at tests/testthat/testdata/pop_a_non_biallelic.vcf"
ls -l tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf 2>/dev/null || echo "File not found at tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf"
```

Length of output: 412

---



</details>

**Correct the test data file path in tests/testthat/_test_gt_tibble_vcf.R**

The file `pop_a_non_biallelic.vcf` lives under `tests/testthat/testdata/vcf/`, not directly in `testdata/`. Please update the path on line 3:

```diff
- vcf_path <- test_path("testdata/pop_a_non_biallelic.vcf")
+ vcf_path <- test_path("testdata/vcf/pop_a_non_biallelic.vcf")
```

This ensures the test will locate the file at its actual location. 

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  vcf_path <- test_path("testdata/vcf/pop_a_non_biallelic.vcf")
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-10T05:52:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-09T16:03:57Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

The update replaces legacy VCF parsing functions and C++ code with a new, faster VCF parser focused on biallelic SNPs and efficient genotype extraction into file-backed matrices. The R interface is refactored to use these new routines, with updated function signatures and documentation. Testing and test data are expanded to cover filtering of non-biallelic variants.

## Changes

| File(s)                                                                                 | Change Summary |
|-----------------------------------------------------------------------------------------|---------------|
| R/RcppExports.R, src/RcppExports.cpp, src/vcf_parser.cpp (removed), src/vcf_parser_fast.cpp (added) | Removed old VCF parsing functions and wrappers; added new `vcf_loci_table` and `vcf_genotypes_to_fbm` functions and wrappers; replaced `extractAltAlleleCountsFromVCF` and `get_ploidy_from_VCF` with new, streamlined C++ parser. |
| R/vcf_to_fbm_cpp_fast.R, man/vcf_to_fbm_cpp.Rd                                          | Introduced new `vcf_to_fbm_cpp` function for VCF-to-FBM conversion; updated documentation to reflect new interface and removed unused parameters. |
| R/gen_tibble_vcf.R                                                                      | Updated `gen_tibble_vcf` function signature for parser selection; changed logic to use new parser and removed `chunk_size` argument for "cpp" parser. |
| R/gen_tibble.R, man/gen_tibble.Rd                                                       | Clarified documentation for `chunk_size` parameter, specifying applicability only for "vcfR" parser. No code logic changed. |
| tests/testthat/test_gt_tibble_vcf.R, tests/testthat/testdata/vcf/pop_a_non_biallelic.vcf| Added test and test data for filtering non-biallelic SNPs; verified correct filtering and error handling for different parsers. |
| man/gt_as_geno_lea.Rd                                                                   | Reformatted description text for readability without changing functionality. |
| src/pairwise_fst_nei87_loop.cpp                                                         | Whitespace and comment formatting changes only; no logic modifications. |
| tests/testthat/test_gen_tibble.R                                                        | Minor whitespace changes in test code; no logic changes. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant R_User
    participant R_Functions
    participant Cpp_Parser
    participant FBM

    R_User->>R_Functions: call vcf_to_fbm_cpp(vcf_path)
    R_Functions->>Cpp_Parser: vcf_loci_table(vcf_path)
    Cpp_Parser-->>R_Functions: loci metadata, sample names, ploidy, biallelic info
    R_Functions->>FBM: initialize FBM (code256) with loci and sample dimensions
    R_Functions->>Cpp_Parser: vcf_genotypes_to_fbm(vcf_path, FBM, biallelic, missing_value, n_header_lines)
    Cpp_Parser-->>FBM: fill FBM with genotype data
    R_Functions->>R_User: return path to bigSNP RDS file
```

## Poem

> In the meadow of code, a new parser hops,  
> Old alleles retire, as efficiency pops!  
> Biallelic dreams in a file-backed bed,  
> Genotypes swiftly, where rabbits once tread.  
> Test files sprout like carrots anew—  
> Filtering the garden for variants true.  
> 🥕✨
"""

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 43995bc6ec7b583d8116c74a3d489aa6010ebe78 and f9b84ab95da47ea58728431a5b6fee7dbb40ff9f.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `R/RcppExports.R` (1 hunks)
* `man/gt_as_geno_lea.Rd` (1 hunks)
* `src/RcppExports.cpp` (2 hunks)
* `src/pairwise_fst_nei87_loop.cpp` (4 hunks)
* `tests/testthat/test_gen_tibble.R` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (3)</summary>

* man/gt_as_geno_lea.Rd
* src/pairwise_fst_nei87_loop.cpp
* tests/testthat/test_gen_tibble.R

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* R/RcppExports.R

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/RcppExports.cpp (1)</summary><blockquote>

<details>
<summary>src/vcf_parser_fast.cpp (4)</summary>

* `vcf_loci_table` (69-195)
* `vcf_loci_table` (69-69)
* `vcf_genotypes_to_fbm` (267-355)
* `vcf_genotypes_to_fbm` (267-271)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: ubuntu-latest (oldrel-1)
* GitHub Check: ubuntu-latest (release)
* GitHub Check: ubuntu-latest (devel)
* GitHub Check: pkgdown
* GitHub Check: windows-latest (release)
* GitHub Check: macos-latest (release)
* GitHub Check: lint
* GitHub Check: test-coverage

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>src/RcppExports.cpp (3)</summary>

`308-318`: **Looks good - New VCF parser function export is properly implemented.**

The `vcf_loci_table` function exports a streamlined interface that returns a comprehensive list of loci metadata, sample names, ploidy information, and biallelic flags. This is consistent with the function implementation in `vcf_parser_fast.cpp`.

---

`319-333`: **Properly implemented function for efficient genotype extraction.**

The `vcf_genotypes_to_fbm` function provides a streamlined interface for extracting genotype data into a file-backed matrix (FBM). The parameter list correctly includes:
- Filename for the VCF
- Environment for the FBM
- IntegerVector of biallelic flags to filter variants
- Missing value constant
- Number of header lines to skip

This approach is more efficient than the previous implementation as it focuses specifically on biallelic SNPs and loads data directly into the FBM.

---

`369-370`: **Correct registration of new VCF parser functions.**

The new functions are properly registered in the CallEntries array with the correct number of parameters (1 for `vcf_loci_table` and 5 for `vcf_genotypes_to_fbm`).

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=EvolEcolGroup/tidypopgen&utm_content=111):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-09T16:11:45Z
body: ## [Codecov](https://app.codecov.io/gh/EvolEcolGroup/tidypopgen/pull/111?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup) Report
Attention: Patch coverage is `95.71429%` with `9 lines` in your changes missing coverage. Please review.
> Project coverage is 92.59%. Comparing base [(`7ce267b`)](https://app.codecov.io/gh/EvolEcolGroup/tidypopgen/commit/7ce267b12a0465033404437bf9e116df111ac865?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup) to head [(`f9b84ab`)](https://app.codecov.io/gh/EvolEcolGroup/tidypopgen/commit/f9b84ab95da47ea58728431a5b6fee7dbb40ff9f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup).
> Report is 1 commits behind head on dev.

| [Files with missing lines](https://app.codecov.io/gh/EvolEcolGroup/tidypopgen/pull/111?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup) | Patch % | Lines |
|---|---|---|
| [src/vcf\_parser\_fast.cpp](https://app.codecov.io/gh/EvolEcolGroup/tidypopgen/pull/111?src=pr&el=tree&filepath=src%2Fvcf_parser_fast.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup#diff-c3JjL3ZjZl9wYXJzZXJfZmFzdC5jcHA=) | 96.79% | [5 Missing :warning: ](https://app.codecov.io/gh/EvolEcolGroup/tidypopgen/pull/111?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup) |
| [R/vcf\_to\_fbm\_cpp\_fast.R](https://app.codecov.io/gh/EvolEcolGroup/tidypopgen/pull/111?src=pr&el=tree&filepath=R%2Fvcf_to_fbm_cpp_fast.R&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup#diff-Ui92Y2ZfdG9fZmJtX2NwcF9mYXN0LlI=) | 92.30% | [4 Missing :warning: ](https://app.codecov.io/gh/EvolEcolGroup/tidypopgen/pull/111?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##              dev     #111      +/-   ##
==========================================
- Coverage   92.65%   92.59%   -0.06%     
==========================================
  Files         119      119              
  Lines        6382     6359      -23     
==========================================
- Hits         5913     5888      -25     
- Misses        469      471       +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/EvolEcolGroup/tidypopgen/pull/111?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EvolEcolGroup).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE