Skip to content

Commit 9dffbea

Browse files
committed
igblastr 1.1.6
Add function compute_auxdata() to compute the auxiliary data for a set of germline J gene allele sequences. Rename functions get_igblast_auxiliary_data() and load_igblast_auxiliary_data() to get_auxdata_path() and load_auxdata(), respectively. The old names can still be used but are deprecated.
1 parent 7cde9dd commit 9dffbea

16 files changed

Lines changed: 738 additions & 217 deletions

‎DESCRIPTION‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Description: The igblastr package provides functions to conveniently install
77
biocViews: Immunology, Immunogenetics, ImmunoOncology, CellBiology
88
URL: https://bioconductor.org/packages/igblastr
99
BugReports: https://github.com/HyrienLab/igblastr/issues
10-
Version: 1.1.5
10+
Version: 1.1.6
1111
License: Artistic-2.0
1212
Encoding: UTF-8
1313
Authors@R: c(
@@ -43,7 +43,8 @@ Collate:
4343
get_igblast_root.R
4444
edit_imgt_file.R
4545
igblast_info.R
46-
igdata-utils.R
46+
update_live_igdata.R
47+
auxdata-utils.R
4748
install_igblast.R
4849
make_blastdbs.R
4950
create_region_db.R

‎NAMESPACE‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import(Biostrings)
2424
###
2525

2626
S3method(print, alignment_summary)
27-
S3method(print, aux_data_md5sum_df)
27+
S3method(print, auxdata_md5sum_df)
2828
S3method(print, c_region_dbs_df)
2929
S3method(print, fmt7footer)
3030
S3method(print, fmt7record)
@@ -47,7 +47,7 @@ S3method(summary, query_details)
4747

4848
export(
4949
print.alignment_summary,
50-
print.aux_data_md5sum_df,
50+
print.auxdata_md5sum_df,
5151
print.c_region_dbs_df,
5252
print.fmt7footer,
5353
print.fmt7record,
@@ -79,10 +79,14 @@ export(
7979
list_igblast_organisms,
8080
igblastn_version, makeblastdb_version, igblast_build, igblast_info,
8181

82-
## igdata-utils.R:
82+
## update_live_igdata.R:
8383
reset_live_igdata, update_live_igdata,
8484
time_since_live_igdata_last_checked, igdata_info,
85-
get_igblast_auxiliary_data, load_igblast_auxiliary_data,
85+
86+
## auxdata-utils.R:
87+
get_auxdata_path,
88+
load_auxdata,
89+
compute_auxdata,
8690

8791
## install_igblast.R:
8892
install_igblast,

‎R/auxdata-utils.R‎

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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+

‎R/builtin_db-utils.R‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ read_version_file <- function(dirpath)
5252
.create_missing_builtin_AIRR_human_germline_dbs <- function(human_dir, destdir)
5353
{
5454
stopifnot(isSingleNonWhiteString(human_dir), dir.exists(human_dir))
55-
human_subdirs <- list.dirs(human_dir, recursive=FALSE)
56-
fasta_stores <- file.path(rep(human_subdirs, each=2L), c("ref", "src"))
55+
human_subdirs <- list.dirs(human_dir, full.names=FALSE, recursive=FALSE)
56+
human_subdirs <- setdiff(human_subdirs, "diffs")
57+
fasta_stores <- file.path(human_dir,
58+
rep(human_subdirs, each=2L), c("ref", "src"))
5759
for (fasta_store in fasta_stores)
5860
.create_builtin_AIRR_human_germline_db(fasta_store, destdir,
5961
only.if.missing=TRUE)

‎R/igblastn-args-utils.R‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@
219219
"start positions for the sequences in the J-region db",
220220
"or NULL"))
221221
if (auxiliary_data == "auto")
222-
return(get_igblast_auxiliary_data(organism))
222+
return(get_auxdata_path(organism))
223223
file_path_as_absolute(auxiliary_data)
224224
}
225225

‎R/summarizeMismatches.R‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@
193193
.extract_rseqs <- function(germline_db, call)
194194
{
195195
stopifnot(is(germline_db, "DNAStringSet"), !is.null(names(germline_db)))
196-
NOT_AN_ALLELE_ID <- "_not_an_allele_id_"
197-
call[is.na(call)] <- NOT_AN_ALLELE_ID
198-
c(germline_db, DNAStringSet(setNames("", NOT_AN_ALLELE_ID)))[call]
196+
NOT_A_VALID_ALLELE_NAME <- "_not_a_valid_allele_name_"
197+
call[is.na(call)] <- NOT_A_VALID_ALLELE_NAME
198+
c(germline_db, DNAStringSet(setNames("", NOT_A_VALID_ALLELE_NAME)))[call]
199199
}
200200

201201
### Returns the positions of the nucleotide mismatches in an IPosList object

0 commit comments

Comments
 (0)