From ae28396faea3dbffaf9d269b730676b35ccda92c Mon Sep 17 00:00:00 2001 From: Cristianetaniguti Date: Thu, 2 Jul 2026 13:10:34 -0400 Subject: [PATCH 1/2] init SAT-151 --- R/check_madc_sanity.R | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/R/check_madc_sanity.R b/R/check_madc_sanity.R index 88f564a..3df4e62 100644 --- a/R/check_madc_sanity.R +++ b/R/check_madc_sanity.R @@ -1,11 +1,11 @@ #' Run basic sanity checks on a MADC-style allele report #' #' @description -#' Performs nine quick validations on an allele report: +#' Performs eleven quick validations on an allele report: #' 1) **Columns** - required columns are present (`CloneID`, `AlleleID`, `AlleleSequence`); #' 2) **FixAlleleIDs** - first column's first up-to-6 rows are not all blank or `"*"` #' *and* both `_0001` and `_0002` appear in `AlleleID`; -#' 3) **IUPACcodes** - presence of non-ATCG characters in `AlleleSequence`; +#' 3) **IUPACcodes** - presence of non-ATCG characters in `AlleleSequence` for Ref_/Alt_ alleles; #' 4) **LowerCase** - presence of lowercase a/t/c/g in `AlleleSequence`; #' 5) **Indels** - reference/alternate allele lengths differ for the same `CloneID`, #' or a `"-"` character is present in `AlleleSequence`; @@ -14,7 +14,8 @@ #' 7) **allNAcol** - at least one column contains only `NA` or empty values; #' 8) **allNArow** - at least one row contains only `NA` or empty values; #' 9) **RefAltSeqs** - every `CloneID` has at least one `Ref` and one `Alt` allele row; -#' 10) **OtherAlleles** - presence of alleles where the target locus differs from both the Ref and Alt in `AlleleSequence`. +#' 10) **OtherAlleles** - presence of alleles where the target locus differs from both the Ref and Alt in `AlleleSequence`; +#' 11) **IUPACcodes_MatchAlleles** - presence of non-ATCG characters in `AlleleSequence` for RefMatch_/AltMatch_ alleles. #' #' @param report A `data.frame` with at least the columns #' `CloneID`, `AlleleID`, and `AlleleSequence`. The first column is also @@ -30,6 +31,8 @@ #' `_0001`/`_0002` suffixes). #' - **IUPAC check:** Flags any character outside `A`, `T`, `C`, `G` and `"-"` #' (case-insensitive), which includes ambiguity codes (`N`, `R`, `Y`, etc.). +#' Checked separately for Ref_/Alt_ alleles (IUPACcodes) and RefMatch_/AltMatch_ +#' alleles (IUPACcodes_MatchAlleles). #' - **Indels:** Rows are split by `AlleleID` containing `"Ref_0001"` vs #' `"Alt_0002"`, merged by `CloneID`, and flagged as indels if either (a) the #' lengths of `AlleleSequence` differ, (b) the sequences have the same length @@ -53,9 +56,10 @@ #' #' @return A named list with five elements: #' \describe{ -#' \item{checks}{Named logical vector with nine entries: +#' \item{checks}{Named logical vector with eleven entries: #' `Columns`, `FixAlleleIDs`, `IUPACcodes`, `LowerCase`, `Indels`, -#' `ChromPos`, `allNAcol`, `allNArow`, `RefAltSeqs`. +#' `ChromPos`, `allNAcol`, `allNArow`, `RefAltSeqs`, `OtherAlleles`, +#' `IUPACcodes_MatchAlleles`. #' `TRUE` means the condition was detected (or passed for `Columns`, #' `FixAlleleIDs`, `ChromPos`, and `RefAltSeqs`); `NA` means the check #' was skipped.} @@ -78,9 +82,11 @@ check_madc_sanity <- function(report) { # Initialize checks <- c(Columns = NA, FixAlleleIDs = NA, IUPACcodes = NA, LowerCase = NA, Indels = NA, - ChromPos = NA, allNAcol = NA, allNArow = NA, RefAltSeqs = NA, OtherAlleles = NA) + ChromPos = NA, allNAcol = NA, allNArow = NA, RefAltSeqs = NA, OtherAlleles = NA, + IUPACcodes_MatchAlleles = NA) messages <- list(Columns = NA, FixAlleleIDs = NA, IUPACcodes = NA, LowerCase = NA, Indels = NA, - ChromPos = NA, allNAcol = NA, allNArow = NA, RefAltSeqs = NA, OtherAlleles = NA) + ChromPos = NA, allNAcol = NA, allNArow = NA, RefAltSeqs = NA, OtherAlleles = NA, + IUPACcodes_MatchAlleles = NA) # ---- FixAlleleIDs ---- # Check if first up-to-6 entries in the *first column* are all "" or "*" @@ -104,8 +110,15 @@ check_madc_sanity <- function(report) { if(checks[["Columns"]]){ # ---- IUPACcodes ---- - iu <- grepl("[^ATCG-]", report$AlleleSequence, ignore.case = TRUE) - checks["IUPACcodes"] <- any(iu, na.rm = TRUE) + # Check for IUPAC codes in Ref_ and Alt_ alleles + ref_alt_rows <- grepl("Ref_|Alt_", report$AlleleID) + iu_ref_alt <- grepl("[^ATCG-]", report$AlleleSequence[ref_alt_rows], ignore.case = TRUE) + checks["IUPACcodes"] <- any(iu_ref_alt, na.rm = TRUE) + + # Check for IUPAC codes in RefMatch_ and AltMatch_ alleles + ref_alt_match_rows <- grepl("RefMatch_|AltMatch_", report$AlleleID) + iu_match <- grepl("[^ATCG-]", report$AlleleSequence[ref_alt_match_rows], ignore.case = TRUE) + checks["IUPACcodes_MatchAlleles"] <- any(iu_match, na.rm = TRUE) # ---- LowerCase ---- lc <- grepl("[atcg]", report$AlleleSequence) @@ -189,8 +202,8 @@ check_madc_sanity <- function(report) { "One or more required columns missing. Verify if your file has columns: CloneID, AlleleID, AlleleSequence") messages[["FixAlleleIDs"]] <- c("Fixed Allele IDs look good", "MADC not processed by HapApp") - messages[["IUPACcodes"]] <- c("IUPAC (non-ATCG) codes found in AlleleSequence. This codes are not currently supported by BIGr/BIGapp. Run HapApp to replace them", - "No IUPAC (non-ATCG) codes found in AlleleSequence") + messages[["IUPACcodes"]] <- c("IUPAC (non-ATCG) codes found in Ref_/Alt_ AlleleSequence. This codes are not currently supported by BIGr/BIGapp. Run HapApp to replace them", + "No IUPAC (non-ATCG) codes found in Ref_/Alt_ AlleleSequence") messages[["LowerCase"]] <- c("Lowercase bases found in AlleleSequence", "No lowercase bases found in AlleleSequence") messages[["Indels"]] <- c(paste("Indels found (ref/alt lengths differ or >1 mismatch between same-length sequences) for the CloneIDs:",paste(indels, collapse = " ")), @@ -207,6 +220,8 @@ check_madc_sanity <- function(report) { "Missing Alt: ", paste(missAlt, collapse = " "), ".")) messages[["OtherAlleles"]] <- c("Alleles other than Ref and Alt were found in AlleleID", "No alleles other than Ref and Alt found in AlleleID") + messages[["IUPACcodes_MatchAlleles"]] <- c("IUPAC (non-ATCG) codes found in RefMatch/AltMatch AlleleSequence. This codes are not currently supported by BIGr/BIGapp they will be ignored in the conversion to VCF", + "No IUPAC (non-ATCG) codes found in RefMatch/AltMatch AlleleSequence") list(checks = checks, messages = messages, indel_clone_ids = indels, missRef = missRef, missAlt = missAlt) From d63168ec7a6cdb15bb09d1c6a1697a5cc48070e2 Mon Sep 17 00:00:00 2001 From: Cristianetaniguti Date: Thu, 2 Jul 2026 17:02:14 -0400 Subject: [PATCH 2/2] Let pass identical IUPAC code in Ref Alt --- R/check_madc_sanity.R | 110 ++++++++++++++++++++++++++++++--------- man/check_madc_sanity.Rd | 31 ++++++++--- 2 files changed, 110 insertions(+), 31 deletions(-) diff --git a/R/check_madc_sanity.R b/R/check_madc_sanity.R index 3df4e62..9905ec4 100644 --- a/R/check_madc_sanity.R +++ b/R/check_madc_sanity.R @@ -1,21 +1,24 @@ #' Run basic sanity checks on a MADC-style allele report #' #' @description -#' Performs eleven quick validations on an allele report: +#' Performs twelve quick validations on an allele report: #' 1) **Columns** - required columns are present (`CloneID`, `AlleleID`, `AlleleSequence`); #' 2) **FixAlleleIDs** - first column's first up-to-6 rows are not all blank or `"*"` #' *and* both `_0001` and `_0002` appear in `AlleleID`; -#' 3) **IUPACcodes** - presence of non-ATCG characters in `AlleleSequence` for Ref_/Alt_ alleles; -#' 4) **LowerCase** - presence of lowercase a/t/c/g in `AlleleSequence`; -#' 5) **Indels** - reference/alternate allele lengths differ for the same `CloneID`, +#' 3) **IUPACcodes** - presence of non-ATCG characters in Ref_/Alt_ `AlleleSequence` +#' that differ in position between Ref and Alt, or are present in only one; +#' 4) **IUPACcodes_IdenticalRefAlt** - presence of Ref_/Alt_ pairs that share IUPAC codes +#' at the same positions in their sequences; +#' 5) **LowerCase** - presence of lowercase a/t/c/g in `AlleleSequence`; +#' 6) **Indels** - reference/alternate allele lengths differ for the same `CloneID`, #' or a `"-"` character is present in `AlleleSequence`; -#' 6) **ChromPos** - all `CloneID` values follow the `Chr_Position` format +#' 7) **ChromPos** - all `CloneID` values follow the `Chr_Position` format #' (prefix matches `"chr"` case-insensitively, suffix is a positive integer); -#' 7) **allNAcol** - at least one column contains only `NA` or empty values; -#' 8) **allNArow** - at least one row contains only `NA` or empty values; -#' 9) **RefAltSeqs** - every `CloneID` has at least one `Ref` and one `Alt` allele row; -#' 10) **OtherAlleles** - presence of alleles where the target locus differs from both the Ref and Alt in `AlleleSequence`; -#' 11) **IUPACcodes_MatchAlleles** - presence of non-ATCG characters in `AlleleSequence` for RefMatch_/AltMatch_ alleles. +#' 8) **allNAcol** - at least one column contains only `NA` or empty values; +#' 9) **allNArow** - at least one row contains only `NA` or empty values; +#' 10) **RefAltSeqs** - every `CloneID` has at least one `Ref` and one `Alt` allele row; +#' 11) **OtherAlleles** - presence of alleles where the target locus differs from both the Ref and Alt in `AlleleSequence`; +#' 12) **IUPACcodes_MatchAlleles** - presence of non-ATCG characters in `AlleleSequence` for RefMatch_/AltMatch_ alleles. #' #' @param report A `data.frame` with at least the columns #' `CloneID`, `AlleleID`, and `AlleleSequence`. The first column is also @@ -31,8 +34,13 @@ #' `_0001`/`_0002` suffixes). #' - **IUPAC check:** Flags any character outside `A`, `T`, `C`, `G` and `"-"` #' (case-insensitive), which includes ambiguity codes (`N`, `R`, `Y`, etc.). -#' Checked separately for Ref_/Alt_ alleles (IUPACcodes) and RefMatch_/AltMatch_ -#' alleles (IUPACcodes_MatchAlleles). +#' For Ref_/Alt_ alleles, two checks are performed: `IUPACcodes` is `TRUE` when +#' any CloneID has IUPAC codes at different positions between Ref and Alt, or +#' IUPAC is present in only one of them. `IUPACcodes_IdenticalRefAlt` is `TRUE` +#' when any CloneID has Ref and Alt with IUPAC codes at exactly the same positions +#' (non-IUPAC bases may still differ). Those CloneIDs are stored in +#' `iupac_identical_clone_ids`. RefMatch_/AltMatch_ alleles are checked +#' separately via `IUPACcodes_MatchAlleles`. #' - **Indels:** Rows are split by `AlleleID` containing `"Ref_0001"` vs #' `"Alt_0002"`, merged by `CloneID`, and flagged as indels if either (a) the #' lengths of `AlleleSequence` differ, (b) the sequences have the same length @@ -54,12 +62,12 @@ #' `FixAlleleIDs` are evaluated; all other checks remain `NA` and #' `indel_clone_ids`, `missRef`, and `missAlt` are returned as `NULL`. #' -#' @return A named list with five elements: +#' @return A named list with six elements: #' \describe{ -#' \item{checks}{Named logical vector with eleven entries: -#' `Columns`, `FixAlleleIDs`, `IUPACcodes`, `LowerCase`, `Indels`, -#' `ChromPos`, `allNAcol`, `allNArow`, `RefAltSeqs`, `OtherAlleles`, -#' `IUPACcodes_MatchAlleles`. +#' \item{checks}{Named logical vector with twelve entries: +#' `Columns`, `FixAlleleIDs`, `IUPACcodes`, `IUPACcodes_IdenticalRefAlt`, +#' `LowerCase`, `Indels`, `ChromPos`, `allNAcol`, `allNArow`, `RefAltSeqs`, +#' `OtherAlleles`, `IUPACcodes_MatchAlleles`. #' `TRUE` means the condition was detected (or passed for `Columns`, #' `FixAlleleIDs`, `ChromPos`, and `RefAltSeqs`); `NA` means the check #' was skipped.} @@ -69,6 +77,11 @@ #' \item{indel_clone_ids}{Character vector of `CloneID`s where ref/alt #' lengths differ. Returns `character(0)` if none are found, or `NULL` #' when required columns are missing.} +#' \item{iupac_identical_clone_ids}{Character vector of `CloneID`s where +#' Ref and Alt alleles have IUPAC codes at the same positions in their +#' sequences (the sequences may differ at standard ATCG positions). Returns +#' `character(0)` if none are found, or `NULL` when required columns are +#' missing.} #' \item{missRef}{Character vector of `CloneID`s that have no `Ref` allele #' row. Returns `character(0)` if all `CloneID`s have a `Ref` row, or #' `NULL` when required columns are missing.} @@ -81,10 +94,12 @@ check_madc_sanity <- function(report) { # Initialize - checks <- c(Columns = NA, FixAlleleIDs = NA, IUPACcodes = NA, LowerCase = NA, Indels = NA, + checks <- c(Columns = NA, FixAlleleIDs = NA, IUPACcodes = NA, IUPACcodes_IdenticalRefAlt = NA, + LowerCase = NA, Indels = NA, ChromPos = NA, allNAcol = NA, allNArow = NA, RefAltSeqs = NA, OtherAlleles = NA, IUPACcodes_MatchAlleles = NA) - messages <- list(Columns = NA, FixAlleleIDs = NA, IUPACcodes = NA, LowerCase = NA, Indels = NA, + messages <- list(Columns = NA, FixAlleleIDs = NA, IUPACcodes = NA, IUPACcodes_IdenticalRefAlt = NA, + LowerCase = NA, Indels = NA, ChromPos = NA, allNAcol = NA, allNArow = NA, RefAltSeqs = NA, OtherAlleles = NA, IUPACcodes_MatchAlleles = NA) @@ -111,9 +126,52 @@ check_madc_sanity <- function(report) { if(checks[["Columns"]]){ # ---- IUPACcodes ---- # Check for IUPAC codes in Ref_ and Alt_ alleles - ref_alt_rows <- grepl("Ref_|Alt_", report$AlleleID) - iu_ref_alt <- grepl("[^ATCG-]", report$AlleleSequence[ref_alt_rows], ignore.case = TRUE) - checks["IUPACcodes"] <- any(iu_ref_alt, na.rm = TRUE) + # Get Ref and Alt sequences + refs <- subset(report, grepl("Ref_", AlleleID), select = c(CloneID, AlleleID, AlleleSequence)) + alts <- subset(report, grepl("Alt_", AlleleID), select = c(CloneID, AlleleID, AlleleSequence)) + + # Identify sequences with IUPAC codes + refs_with_iupac <- refs[grepl("[^ATCG-]", refs$AlleleSequence, ignore.case = TRUE), ] + alts_with_iupac <- alts[grepl("[^ATCG-]", alts$AlleleSequence, ignore.case = TRUE), ] + + # Merge Ref and Alt by CloneID to compare sequences + merged_iupac <- merge(refs_with_iupac, alts_with_iupac, by = "CloneID", + suffixes = c("_ref", "_alt"), all = FALSE) + + # Check if IUPAC codes are at the same positions in both sequences + if (nrow(merged_iupac) > 0) { + identical_iupac_positions <- vapply(seq_len(nrow(merged_iupac)), function(i) { + ref_seq <- toupper(merged_iupac$AlleleSequence_ref[i]) + alt_seq <- toupper(merged_iupac$AlleleSequence_alt[i]) + + # Split sequences into characters + ref_chars <- strsplit(ref_seq, "")[[1]] + alt_chars <- strsplit(alt_seq, "")[[1]] + + # Check if same length + if (length(ref_chars) != length(alt_chars)) return(FALSE) + + # Check if IUPAC codes are at the same positions + ref_is_iupac <- !ref_chars %in% c("A", "T", "C", "G", "-") + alt_is_iupac <- !alt_chars %in% c("A", "T", "C", "G", "-") + + # IUPAC codes must be at exactly the same positions + all(ref_is_iupac == alt_is_iupac) + }, logical(1)) + + iupac_identical_clone_ids <- merged_iupac$CloneID[identical_iupac_positions] + } else { + iupac_identical_clone_ids <- character(0) + } + + # Get all CloneIDs with IUPAC in either Ref or Alt + all_iupac_clones <- unique(c(refs_with_iupac$CloneID, alts_with_iupac$CloneID)) + # IUPAC codes that differ between Ref and Alt (or present in only one) + differing_iupac_clones <- setdiff(all_iupac_clones, iupac_identical_clone_ids) + + # Only flag as TRUE if there are IUPAC codes that differ + checks["IUPACcodes"] <- length(differing_iupac_clones) > 0 + checks["IUPACcodes_IdenticalRefAlt"] <- length(iupac_identical_clone_ids) > 0 # Check for IUPAC codes in RefMatch_ and AltMatch_ alleles ref_alt_match_rows <- grepl("RefMatch_|AltMatch_", report$AlleleID) @@ -194,6 +252,7 @@ check_madc_sanity <- function(report) { } else { indels <- NULL + iupac_identical_clone_ids <- NULL missRef <- NULL missAlt <- NULL } @@ -202,8 +261,10 @@ check_madc_sanity <- function(report) { "One or more required columns missing. Verify if your file has columns: CloneID, AlleleID, AlleleSequence") messages[["FixAlleleIDs"]] <- c("Fixed Allele IDs look good", "MADC not processed by HapApp") - messages[["IUPACcodes"]] <- c("IUPAC (non-ATCG) codes found in Ref_/Alt_ AlleleSequence. This codes are not currently supported by BIGr/BIGapp. Run HapApp to replace them", - "No IUPAC (non-ATCG) codes found in Ref_/Alt_ AlleleSequence") + messages[["IUPACcodes"]] <- c("IUPAC (non-ATCG) codes found in Ref_/Alt_ AlleleSequence that differ between alleles or are present in only one. These codes are not currently supported by BIGr/BIGapp", + "No differing IUPAC (non-ATCG) codes found in Ref_/Alt_ AlleleSequence") + messages[["IUPACcodes_IdenticalRefAlt"]] <- c("IUPAC (non-ATCG) codes found at identical positions in Ref and Alt AlleleSequence. These codes will be handled during VCF conversion", + "No IUPAC (non-ATCG) codes found at identical positions in Ref and Alt AlleleSequence") messages[["LowerCase"]] <- c("Lowercase bases found in AlleleSequence", "No lowercase bases found in AlleleSequence") messages[["Indels"]] <- c(paste("Indels found (ref/alt lengths differ or >1 mismatch between same-length sequences) for the CloneIDs:",paste(indels, collapse = " ")), @@ -224,6 +285,7 @@ check_madc_sanity <- function(report) { "No IUPAC (non-ATCG) codes found in RefMatch/AltMatch AlleleSequence") list(checks = checks, messages = messages, indel_clone_ids = indels, + iupac_identical_clone_ids = iupac_identical_clone_ids, missRef = missRef, missAlt = missAlt) } diff --git a/man/check_madc_sanity.Rd b/man/check_madc_sanity.Rd index 1d7eebb..db8dbcc 100644 --- a/man/check_madc_sanity.Rd +++ b/man/check_madc_sanity.Rd @@ -14,11 +14,12 @@ If \code{FixAlleleIDs} is \code{FALSE} (raw DArT format), the first 7 rows are treated as header filler and skipped before further checks are run.} } \value{ -A named list with five elements: +A named list with six elements: \describe{ -\item{checks}{Named logical vector with nine entries: -\code{Columns}, \code{FixAlleleIDs}, \code{IUPACcodes}, \code{LowerCase}, \code{Indels}, -\code{ChromPos}, \code{allNAcol}, \code{allNArow}, \code{RefAltSeqs}. +\item{checks}{Named logical vector with twelve entries: +\code{Columns}, \code{FixAlleleIDs}, \code{IUPACcodes}, \code{IUPACcodes_IdenticalRefAlt}, +\code{LowerCase}, \code{Indels}, \code{ChromPos}, \code{allNAcol}, \code{allNArow}, \code{RefAltSeqs}, +\code{OtherAlleles}, \code{IUPACcodes_MatchAlleles}. \code{TRUE} means the condition was detected (or passed for \code{Columns}, \code{FixAlleleIDs}, \code{ChromPos}, and \code{RefAltSeqs}); \code{NA} means the check was skipped.} @@ -28,6 +29,11 @@ when it is \code{FALSE}. Indexed by the same names as \code{checks}.} \item{indel_clone_ids}{Character vector of \code{CloneID}s where ref/alt lengths differ. Returns \code{character(0)} if none are found, or \code{NULL} when required columns are missing.} +\item{iupac_identical_clone_ids}{Character vector of \code{CloneID}s where +Ref and Alt alleles have IUPAC codes at the same positions in their +sequences (the sequences may differ at standard ATCG positions). Returns +\code{character(0)} if none are found, or \code{NULL} when required columns are +missing.} \item{missRef}{Character vector of \code{CloneID}s that have no \code{Ref} allele row. Returns \code{character(0)} if all \code{CloneID}s have a \code{Ref} row, or \code{NULL} when required columns are missing.} @@ -37,12 +43,15 @@ row. Returns \code{character(0)} if all \code{CloneID}s have an \code{Alt} row, } } \description{ -Performs nine quick validations on an allele report: +Performs twelve quick validations on an allele report: \enumerate{ \item \strong{Columns} - required columns are present (\code{CloneID}, \code{AlleleID}, \code{AlleleSequence}); \item \strong{FixAlleleIDs} - first column's first up-to-6 rows are not all blank or \code{"*"} \emph{and} both \verb{_0001} and \verb{_0002} appear in \code{AlleleID}; -\item \strong{IUPACcodes} - presence of non-ATCG characters in \code{AlleleSequence}; +\item \strong{IUPACcodes} - presence of non-ATCG characters in Ref_/Alt_ \code{AlleleSequence} +that differ in position between Ref and Alt, or are present in only one; +\item \strong{IUPACcodes_IdenticalRefAlt} - presence of Ref_/Alt_ pairs that share IUPAC codes +at the same positions in their sequences; \item \strong{LowerCase} - presence of lowercase a/t/c/g in \code{AlleleSequence}; \item \strong{Indels} - reference/alternate allele lengths differ for the same \code{CloneID}, or a \code{"-"} character is present in \code{AlleleSequence}; @@ -51,7 +60,8 @@ or a \code{"-"} character is present in \code{AlleleSequence}; \item \strong{allNAcol} - at least one column contains only \code{NA} or empty values; \item \strong{allNArow} - at least one row contains only \code{NA} or empty values; \item \strong{RefAltSeqs} - every \code{CloneID} has at least one \code{Ref} and one \code{Alt} allele row; -\item \strong{OtherAlleles} - presence of alleles where the target locus differs from both the Ref and Alt in \code{AlleleSequence}. +\item \strong{OtherAlleles} - presence of alleles where the target locus differs from both the Ref and Alt in \code{AlleleSequence}; +\item \strong{IUPACcodes_MatchAlleles} - presence of non-ATCG characters in \code{AlleleSequence} for RefMatch_/AltMatch_ alleles. } } \details{ @@ -63,6 +73,13 @@ first 7 rows are dropped before subsequent checks are run. The check is \verb{_0001}/\verb{_0002} suffixes). \item \strong{IUPAC check:} Flags any character outside \code{A}, \code{T}, \code{C}, \code{G} and \code{"-"} (case-insensitive), which includes ambiguity codes (\code{N}, \code{R}, \code{Y}, etc.). +For Ref_/Alt_ alleles, two checks are performed: \code{IUPACcodes} is \code{TRUE} when +any CloneID has IUPAC codes at different positions between Ref and Alt, or +IUPAC is present in only one of them. \code{IUPACcodes_IdenticalRefAlt} is \code{TRUE} +when any CloneID has Ref and Alt with IUPAC codes at exactly the same positions +(non-IUPAC bases may still differ). Those CloneIDs are stored in +\code{iupac_identical_clone_ids}. RefMatch_/AltMatch_ alleles are checked +separately via \code{IUPACcodes_MatchAlleles}. \item \strong{Indels:} Rows are split by \code{AlleleID} containing \code{"Ref_0001"} vs \code{"Alt_0002"}, merged by \code{CloneID}, and flagged as indels if either (a) the lengths of \code{AlleleSequence} differ, (b) the sequences have the same length