|
| 1 | +### ========================================================================= |
| 2 | +### Access or generate IgBLAST auxiliary data |
| 3 | +### ------------------------------------------------------------------------- |
| 4 | +### |
| 5 | + |
| 6 | + |
| 7 | +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 8 | +### get_auxdata_path() |
| 9 | +### |
| 10 | + |
| 11 | +get_auxdata_path <- function(organism, which=c("live", "original")) |
| 12 | +{ |
| 13 | + organism <- normalize_igblast_organism(organism) |
| 14 | + which <- match.arg(which) |
| 15 | + auxdir <- file.path(path_to_igdata(which), "optional_file") |
| 16 | + auxfile <- file.path(auxdir, paste0(organism, "_gl.aux")) |
| 17 | + if (!file.exists(auxfile)) |
| 18 | + stop(wmsg("no auxiliary data found in ", auxdir, " for ", organism)) |
| 19 | + auxfile |
| 20 | +} |
| 21 | + |
| 22 | +get_igblast_auxiliary_data <- function(...) |
| 23 | +{ |
| 24 | + .Deprecated("get_auxdata_path") |
| 25 | + get_auxdata_path(...) |
| 26 | +} |
| 27 | + |
| 28 | +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 29 | +### load_auxdata() |
| 30 | +### |
| 31 | + |
| 32 | +### 'x' must be a list of character vectors of variable length. |
| 33 | +### Conceptually right-pads the list elements with empty strings ("") |
| 34 | +### to make the list "constant-width" before unlisting it. |
| 35 | +### Returns a character vector of length 'length(x) * width'. |
| 36 | +.right_pad_with_empty_strings_and_unlist <- function(x, width=NA) |
| 37 | +{ |
| 38 | + stopifnot(is.list(x), isSingleNumberOrNA(width)) |
| 39 | + x_len <- length(x) |
| 40 | + if (x_len == 0L) |
| 41 | + return(character(0)) |
| 42 | + x_lens <- lengths(x) |
| 43 | + max_x_lens <- max(x_lens) |
| 44 | + if (is.na(width)) { |
| 45 | + width <- max_x_lens |
| 46 | + } else { |
| 47 | + width <- as.integer(width) |
| 48 | + stopifnot(width >= max_x_lens) |
| 49 | + } |
| 50 | + y_lens <- width - x_lens |
| 51 | + x_seqalong <- seq_along(x) |
| 52 | + f <- rep.int(x_seqalong, y_lens) |
| 53 | + attributes(f) <- list(levels=as.character(x_seqalong), class="factor") |
| 54 | + y <- split(character(length(f)), f) |
| 55 | + collate_subscript <- rep(x_seqalong, each=2L) |
| 56 | + collate_subscript[2L * x_seqalong] <- x_seqalong + x_len |
| 57 | + unlist(c(x, y)[collate_subscript], recursive=FALSE, use.names=FALSE) |
| 58 | +} |
| 59 | + |
| 60 | +### Returns the table in a character matrix. |
| 61 | +.read_jagged_table_as_character_matrix <- function(file) |
| 62 | +{ |
| 63 | + lines <- readLines(file) |
| 64 | + lines <- lines[nzchar(lines) & !has_prefix(lines, "#")] |
| 65 | + data <- strsplit(lines, split="[ \t]+") |
| 66 | + data <- .right_pad_with_empty_strings_and_unlist(data) |
| 67 | + matrix(data, nrow=length(lines), byrow=TRUE) |
| 68 | +} |
| 69 | + |
| 70 | +.make_auxiliary_data_df_from_matrix <- function(m, filepath) |
| 71 | +{ |
| 72 | + if (ncol(m) != 5L) |
| 73 | + stop(wmsg("error loading ", filepath, ": unexpected number of fields")) |
| 74 | + data.frame( |
| 75 | + allele_name = m[ , 1L], |
| 76 | + coding_frame_start=as.integer(m[ , 2L]), |
| 77 | + chain_type = m[ , 3L], |
| 78 | + CDR3_stop =as.integer(m[ , 4L]), |
| 79 | + extra_bps =as.integer(m[ , 5L]) |
| 80 | + ) |
| 81 | +} |
| 82 | + |
| 83 | +### IgBLAST *.aux files are supposedly "tab-delimited" but they are |
| 84 | +### broken in various ways: |
| 85 | +### - they use a mix of whitespaces for the field separators; |
| 86 | +### - each line contains a variable number of fields; |
| 87 | +### - some lines contain trailing whitespaces. |
| 88 | +### So we cannot read them with read.table(). |
| 89 | +load_auxdata <- function(organism, which=c("live", "original")) |
| 90 | +{ |
| 91 | + which <- match.arg(which) |
| 92 | + filepath <- get_auxdata_path(organism, which) |
| 93 | + m <- .read_jagged_table_as_character_matrix(filepath) |
| 94 | + .make_auxiliary_data_df_from_matrix(m, filepath) |
| 95 | +} |
| 96 | + |
| 97 | +load_igblast_auxiliary_data <- function(...) |
| 98 | +{ |
| 99 | + .Deprecated("") |
| 100 | + load_auxdata(...) |
| 101 | +} |
| 102 | + |
| 103 | + |
| 104 | +### - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
| 105 | +### compute_auxdata() |
| 106 | +### |
| 107 | + |
| 108 | +### The FWR4 region is expected to **always** start with AA pattern "WGXG" |
| 109 | +### on the heavy chain. |
| 110 | +.WGXG_pattern <- "TGGGGNNNNGGN" # reverse-translation of "WGXG" |
| 111 | + |
| 112 | +### Returns a named integer vector parallel to 'J_alleles' that contains |
| 113 | +### the **0-based** FWR4 start position for each sequence in 'J_alleles'. |
| 114 | +### 'FWR4_starts' will be set to NA for alleles that don't have a match. |
| 115 | +### For alleles with more than one match, we keep the first match only. |
| 116 | +### The names on the returned vector indicate the AA pattern that was used |
| 117 | +### to determine the start of the FWR4 region. |
| 118 | +.find_heavy_FWR4_starts <- function(J_alleles) |
| 119 | +{ |
| 120 | + stopifnot(is(J_alleles, "DNAStringSet")) |
| 121 | + m <- vmatchPattern(.WGXG_pattern, J_alleles, fixed=FALSE) |
| 122 | + ans <- as.integer(heads(start(m), n=1L)) - 1L |
| 123 | + names(ans) <- ifelse(is.na(ans), NA_character_, "WGXG") |
| 124 | + ans |
| 125 | +} |
| 126 | + |
| 127 | +### The FWR4 region is expected to **always** start with AA pattern "FGXG" |
| 128 | +### on the light chain. |
| 129 | +.FGXG_pattern <- "TTYGGNNNNGGN" # reverse-translation of "FGXG" |
| 130 | + |
| 131 | +### EXPERIMENTAL! |
| 132 | +### The "FGXG" pattern is not found for 4 J alleles in |
| 133 | +### IMGT-202531-1.Mus_musculus.IGH+IGK+IGL: IGKJ3*01, IGKJ3*02, |
| 134 | +### IGLJ2P*01, IGLJ3P*01. However, except for IGLJ2P*01, these alleles |
| 135 | +### are annotated in mouse_gl.aux with a CDR3 stop reported at position 6 |
| 136 | +### (0-based). Turns out that for the 3 alleles annotated in mouse_gl.aux, |
| 137 | +### the two first codons of the FWR4 region translate to AA sequence "FS". |
| 138 | +### Is this a coincidence or does the FS sequence actually play a role on |
| 139 | +### the light chain? What do biologists say about this? In particular, does |
| 140 | +### it make sense to use this alternative pattern to identify the start of |
| 141 | +### the FWR4 region on the light chain when the "FGXG" pattern is not found? |
| 142 | +### Note that all the possible reverse-translations of FS cannot be |
| 143 | +### represented with a single DNA pattern (even with the use of IUPAC |
| 144 | +### ambiguity codes). |
| 145 | +.FS_pattern1 <- "TTYTCN" |
| 146 | +.FS_pattern2 <- "TTYAGY" |
| 147 | + |
| 148 | +### UPDATE on using the "FS" pattern to identify the start of the FWR4 |
| 149 | +### region on the light chain when the "FGXG" pattern is not found: |
| 150 | +### Works well for IMGT-202531-1.Mus_musculus.IGH+IGK+IGL (well, it was |
| 151 | +### specifically designed for that so no surprise here), but not |
| 152 | +### so well for IMGT-202531-1.Rattus_norvegicus.IGH+IGK+IGL or |
| 153 | +### IMGT-202531-1.Oryctolagus_cuniculus.IGH+IGK+IGL (rabbit) |
| 154 | +### or IMGT-202531-1.Macaca_mulatta.IGH+IGK+IGL (rhesus monkey). |
| 155 | +### So we disabled this feature in .find_light_FWR4_starts() below. |
| 156 | + |
| 157 | +.find_light_FWR4_starts <- function(J_alleles) |
| 158 | +{ |
| 159 | + stopifnot(is(J_alleles, "DNAStringSet")) |
| 160 | + m <- vmatchPattern(.FGXG_pattern, J_alleles, fixed=FALSE) |
| 161 | + FGXG_starts <- as.integer(heads(start(m), n=1L)) |
| 162 | + names(FGXG_starts) <- ifelse(is.na(FGXG_starts), NA_character_, "FGXG") |
| 163 | + ## Disabling search for alternative "FS" pattern for now. |
| 164 | + #na_idx <- which(is.na(FGXG_starts)) |
| 165 | + #if (length(na_idx) != 0L) { |
| 166 | + # dangling_alleles <- J_alleles[na_idx] |
| 167 | + # m <- vmatchPattern(.FS_pattern1, dangling_alleles, fixed=FALSE) |
| 168 | + # FS_starts1 <- as.integer(heads(start(m), n=1L)) |
| 169 | + # m <- vmatchPattern(.FS_pattern2, dangling_alleles, fixed=FALSE) |
| 170 | + # FS_starts2 <- as.integer(heads(start(m), n=1L)) |
| 171 | + # FS_starts <- pmin(FS_starts1, FS_starts2, na.rm=TRUE) |
| 172 | + # FGXG_starts[na_idx] <- FS_starts |
| 173 | + # names(FGXG_starts)[na_idx] <- |
| 174 | + # ifelse(is.na(FS_starts), NA_character_, "FS") |
| 175 | + #} |
| 176 | + FGXG_starts - 1L |
| 177 | +} |
| 178 | + |
| 179 | +.VALID_J_GROUPS <- paste0("IG", c("H", "K", "L"), "J") |
| 180 | + |
| 181 | +### Returns a data.frame with the same column names as the data.frame |
| 182 | +### returned by .make_auxiliary_data_df_from_matrix() above, plus |
| 183 | +### the "FWR4_start_pattern" column. |
| 184 | +### NOTE: We set coding_frame_start/CDR3_stop/extra_bps/FWR4_start_pattern |
| 185 | +### to NA for alleles for which the FWR4 start cannot be determined. |
| 186 | +.compute_auxdata_for_J_group <- function(J_alleles, J_group) |
| 187 | +{ |
| 188 | + stopifnot(is(J_alleles, "DNAStringSet"), J_group %in% .VALID_J_GROUPS) |
| 189 | + allele_names <- names(J_alleles) |
| 190 | + stopifnot(!is.null(allele_names)) |
| 191 | + if (length(J_alleles) == 0L) { |
| 192 | + chain_type <- character(0) |
| 193 | + } else { |
| 194 | + allele_groups <- substr(allele_names, 1L, 4L) |
| 195 | + stopifnot(all(allele_groups == J_group)) |
| 196 | + chain_type <- paste0("J", substr(J_group, 3L, 3L)) |
| 197 | + } |
| 198 | + if (J_group == "IGHJ") { |
| 199 | + FWR4_starts <- .find_heavy_FWR4_starts(J_alleles) |
| 200 | + } else { |
| 201 | + FWR4_starts <- .find_light_FWR4_starts(J_alleles) |
| 202 | + } |
| 203 | + coding_frame_starts <- FWR4_starts %% 3L |
| 204 | + extra_bps <- (width(J_alleles) - coding_frame_starts) %% 3L |
| 205 | + data.frame( |
| 206 | + allele_name =allele_names, |
| 207 | + coding_frame_start=coding_frame_starts, |
| 208 | + chain_type =chain_type, |
| 209 | + CDR3_stop =FWR4_starts - 1L, # 0-based |
| 210 | + extra_bps =extra_bps |
| 211 | + ## Returning this column only made sense when we were using "FS" |
| 212 | + ## pattern as a 2nd-chance pattern on the light chain. |
| 213 | + #FWR4_start_pattern=names(FWR4_starts) |
| 214 | + ) |
| 215 | +} |
| 216 | + |
| 217 | +.has_stop_codon <- function(J_alleles, auxdata) |
| 218 | +{ |
| 219 | + stopifnot(is(J_alleles, "DNAStringSet"), is.data.frame(auxdata), |
| 220 | + identical(names(J_alleles), auxdata[ , "allele_name"])) |
| 221 | + ans <- rep.int(NA, length(J_alleles)) |
| 222 | + coding_frame_start <- auxdata[ , "coding_frame_start"] |
| 223 | + searchme_idx <- which(!is.na(coding_frame_start)) |
| 224 | + if (length(searchme_idx) != 0L) { |
| 225 | + alleles <- J_alleles[searchme_idx] |
| 226 | + Ltrim <- coding_frame_start[searchme_idx] |
| 227 | + Rtrim <- (width(alleles) - Ltrim) %% 3L |
| 228 | + trimmed <- narrow(alleles, start=Ltrim+1L, end=-Rtrim-1L) |
| 229 | + alleles_aa <- translate(trimmed, no.init.codon=TRUE) |
| 230 | + ans[searchme_idx] <- vcountPattern("*", alleles_aa) != 0L |
| 231 | + } |
| 232 | + ans |
| 233 | +} |
| 234 | + |
| 235 | +### Returns a data.frame with 1 row per sequence in 'J_alleles'. |
| 236 | +compute_auxdata <- function(J_alleles) |
| 237 | +{ |
| 238 | + if (!is(J_alleles, "DNAStringSet")) |
| 239 | + stop(wmsg("'J_alleles' must be DNAStringSet object")) |
| 240 | + allele_names <- names(J_alleles) |
| 241 | + if (is.null(allele_names)) |
| 242 | + stop(wmsg("'J_alleles' must have names")) |
| 243 | + allele_groups <- substr(allele_names, 1L, 4L) |
| 244 | + if (!all(allele_groups %in% .VALID_J_GROUPS)) |
| 245 | + stop(wmsg("all allele names must start with 'IG[HKL]J'")) |
| 246 | + |
| 247 | + JH_alleles <- J_alleles[allele_groups == "IGHJ"] |
| 248 | + JK_alleles <- J_alleles[allele_groups == "IGKJ"] |
| 249 | + JL_alleles <- J_alleles[allele_groups == "IGLJ"] |
| 250 | + JH_df <- .compute_auxdata_for_J_group(JH_alleles, "IGHJ") |
| 251 | + JK_df <- .compute_auxdata_for_J_group(JK_alleles, "IGKJ") |
| 252 | + JL_df <- .compute_auxdata_for_J_group(JL_alleles, "IGLJ") |
| 253 | + ans <- rbind(JH_df, JK_df, JL_df) |
| 254 | + |
| 255 | + i <- match(allele_names, ans[ , "allele_name"]) |
| 256 | + ans <- S4Vectors:::extract_data_frame_rows(ans, i) |
| 257 | + rownames(ans) <- NULL |
| 258 | + ans$has_stop_codon <- .has_stop_codon(J_alleles, ans) |
| 259 | + |
| 260 | + ## Warn user if CDR3 stop not found for some alleles. |
| 261 | + bad_idx <- which(is.na(ans[ , "CDR3_stop"])) |
| 262 | + if (length(bad_idx) != 0L) { |
| 263 | + in1string <- paste(ans[bad_idx, "allele_name"], collapse=", ") |
| 264 | + warning(wmsg("CDR3 stop not found for allele(s): ", in1string), |
| 265 | + "\n ", |
| 266 | + wmsg("--> coding_frame_start/CDR3_stop/extra_bps ", |
| 267 | + "were set to NA for these alleles")) |
| 268 | + } |
| 269 | + |
| 270 | + ## Warn user if stop codon found in some alleles. |
| 271 | + bad_idx <- which(ans[ , "has_stop_codon"]) |
| 272 | + if (length(bad_idx) != 0L) { |
| 273 | + in1string <- paste(ans[bad_idx, "allele_name"], collapse=", ") |
| 274 | + warning(wmsg("stop codon found in allele(s): ", in1string)) |
| 275 | + } |
| 276 | + |
| 277 | + ans |
| 278 | +} |
| 279 | + |
0 commit comments