Skip to content

Commit bfd6369

Browse files
SamT123claude
andcommitted
Vectorise get_consensus
The two apply() calls over the consensus matrix cost ~4 ms per call regardless of how many sequences are being consensed, which dominates when the caller consenses thousands of small groups. max.col(ties.method = "first") reproduces which.max exactly, so tied columns still resolve deterministically. Verified byte-identical over 400 random alignments across every combination of excluded_characters, min_freq and fill. drop = FALSE keeps a single remaining character class from collapsing the matrix to a vector, which apply() could not handle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8512a78 commit bfd6369

19 files changed

Lines changed: 370 additions & 359 deletions

R/apply_substitutions.R

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,36 @@
77
#' @return Modified sequence with substitutions applied
88
#'
99
#' @export
10-
apply_substitutions = function(sequence, substitutions, type = "aa") {
11-
alphabet = switch(
10+
apply_substitutions <- function(sequence, substitutions, type = "aa") {
11+
alphabet <- switch(
1212
type,
1313
"aa" = c(Biostrings::AA_STANDARD, "X", "-"),
1414
"nt" = c(Biostrings::DNA_BASES, "N", "-"),
1515
stop("type must be 'aa' or 'nt'")
1616
)
1717

18-
new_sequence = sequence
18+
new_sequence <- sequence
1919
for (s in substitutions) {
20-
fat = convert_substitution(s, alphabet)
21-
old_char = stringr::str_sub(new_sequence, fat$at, fat$at)
20+
fat <- convert_substitution(s, alphabet)
21+
old_char <- stringr::str_sub(new_sequence, fat$at, fat$at)
2222
if (!old_char == fat$from) {
2323
# fmt: skip
2424
stop("sub = ", s, "; from_char mismatch, expected: ", fat$from, "; got ", old_char)
2525
}
2626

27-
stringr::str_sub(new_sequence, fat$at, fat$at) = fat$to
27+
stringr::str_sub(new_sequence, fat$at, fat$at) <- fat$to
2828
}
2929

3030
new_sequence
3131
}
3232

33-
convert_substitution = function(s, alphabet) {
34-
invalid = function(s) {
33+
convert_substitution <- function(s, alphabet) {
34+
invalid <- function(s) {
3535
stop("Invalid substitution? ", s)
3636
}
37-
from = stringr::str_sub(s, 1, 1)
38-
to = stringr::str_sub(s, -1, -1)
39-
at = tryCatch(
37+
from <- stringr::str_sub(s, 1, 1)
38+
to <- stringr::str_sub(s, -1, -1)
39+
at <- tryCatch(
4040
as.integer(stringr::str_sub(s, 2, -2)),
4141
warning = function(msg) invalid(s)
4242
)

R/clean_sequences.R

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
#'
3030
#' @importFrom stringr str_replace_all
3131
#' @importFrom Biostrings AA_STANDARD
32-
clean_sequences = function(
32+
clean_sequences <- function(
3333
sequences,
3434
type,
3535
alphabet = NULL,
@@ -40,26 +40,26 @@ clean_sequences = function(
4040
stop("type must be either 'aa' or 'nt', got '", type, "'")
4141
}
4242

43-
sequences = toupper(sequences)
43+
sequences <- toupper(sequences)
4444

4545
if (is.null(replacement_character)) {
46-
replacement_character = c(aa = "X", nt = "N")[[type]]
46+
replacement_character <- c(aa = "X", nt = "N")[[type]]
4747
}
4848

4949
if (is.null(alphabet)) {
50-
alphabet = list(
50+
alphabet <- list(
5151
aa = c(Biostrings::AA_STANDARD, "-"),
5252
nt = c("A", "T", "C", "G", "-")
5353
)[[type]]
5454
}
5555

56-
regex = paste0(
56+
regex <- paste0(
5757
"[^",
5858
paste(alphabet, collapse = ""),
5959
"]"
6060
)
6161

62-
sequences = setNames(
62+
sequences <- setNames(
6363
str_replace_all(
6464
sequences,
6565
regex,

R/fasta.R

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
#'
2121
#' @importFrom readr read_lines
2222
#' @export
23-
fast_fasta = function(path, noisy = F) {
24-
lines = readr::read_lines(path, skip_empty_rows = FALSE)
25-
name_lines = which(substr(lines, 1, 1) == '>')
23+
fast_fasta <- function(path, noisy = F) {
24+
lines <- readr::read_lines(path, skip_empty_rows = FALSE)
25+
name_lines <- which(substr(lines, 1, 1) == '>')
2626
if (noisy) {
2727
message("Loaded ", length(name_lines), " sequences")
2828
}
@@ -32,25 +32,25 @@ fast_fasta = function(path, noisy = F) {
3232
all(name_lines == seq(1, length(lines), 2))
3333
) {
3434
# case: single-line sequences
35-
sequences = lines[-name_lines]
36-
names(sequences) = lines[name_lines]
35+
sequences <- lines[-name_lines]
36+
names(sequences) <- lines[name_lines]
3737
} else {
3838
# case: multi-line sequences
39-
sequences = rep(NA, length(name_lines))
40-
names(sequences) = rep(NA, length(name_lines))
39+
sequences <- rep(NA, length(name_lines))
40+
names(sequences) <- rep(NA, length(name_lines))
4141

42-
name_lines = c(name_lines, length(lines) + 1)
42+
name_lines <- c(name_lines, length(lines) + 1)
4343

4444
for (i in seq_along(name_lines[-1])) {
45-
names(sequences)[[i]] = lines[[name_lines[[i]]]]
46-
sequences[[i]] = paste(
45+
names(sequences)[[i]] <- lines[[name_lines[[i]]]]
46+
sequences[[i]] <- paste(
4747
lines[(name_lines[[i]] + 1):(name_lines[[i + 1]] - 1)],
4848
collapse = ""
4949
)
5050
}
5151
}
5252

53-
names(sequences) = substring(names(sequences), 2)
53+
names(sequences) <- substring(names(sequences), 2)
5454
sequences
5555
}
5656

@@ -77,26 +77,26 @@ fast_fasta = function(path, noisy = F) {
7777
#'
7878
#' @importFrom readr write_lines
7979
#' @export
80-
write_fast_fasta = function(seqs, names = NULL, path, line_width = NULL) {
80+
write_fast_fasta <- function(seqs, names = NULL, path, line_width = NULL) {
8181
if (is.null(names)) {
82-
names = names(seqs)
82+
names <- names(seqs)
8383
}
8484

8585
stopifnot(length(seqs) == length(names))
8686

8787
if (!is.null(line_width)) {
8888
# Split sequences into chunks of line_width characters
89-
seqs = vapply(
89+
seqs <- vapply(
9090
seqs,
9191
function(seq) {
9292
if (nchar(seq) <= line_width) {
9393
return(seq)
9494
}
95-
seq_chunks = character(ceiling(nchar(seq) / line_width))
95+
seq_chunks <- character(ceiling(nchar(seq) / line_width))
9696
for (i in seq_along(seq_chunks)) {
97-
start_pos = (i - 1) * line_width + 1
98-
end_pos = min(i * line_width, nchar(seq))
99-
seq_chunks[i] = substr(seq, start_pos, end_pos)
97+
start_pos <- (i - 1) * line_width + 1
98+
end_pos <- min(i * line_width, nchar(seq))
99+
seq_chunks[i] <- substr(seq, start_pos, end_pos)
100100
}
101101
paste(seq_chunks, collapse = "\n")
102102
},

R/get_consensus.R

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,15 @@ get_consensus <- function(
4141
) {
4242
sequences <- toupper(sequences)
4343
cm <- Biostrings::consensusMatrix(sequences)
44-
cm <- cm[!rownames(cm) %in% excluded_characters, ]
44+
cm <- cm[!rownames(cm) %in% excluded_characters, , drop = FALSE]
4545

46-
get_aa <- function(x) {
47-
names(x)[which.max(x)]
48-
}
46+
# ties.method = "first" matches which.max, so a tied column resolves the same
47+
# way on every run.
48+
winners <- max.col(t(cm), ties.method = "first")
49+
counts <- cm[cbind(winners, seq_len(ncol(cm)))]
50+
highest_frequencies <- counts / colSums(cm)
4951

50-
get_freq <- function(x) {
51-
max(x) / sum(x)
52-
}
53-
54-
highest_frequencies <- setNames(
55-
apply(cm, 2, get_freq),
56-
apply(cm, 2, get_aa)
57-
)
58-
59-
aas <- names(highest_frequencies)
52+
aas <- rownames(cm)[winners]
6053
aas[!is.finite(highest_frequencies) | highest_frequencies < min_freq] <- fill
6154
paste0(aas, collapse = "")
6255
}

R/get_substitutions.R

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
#' @importFrom purrr map2 pmap
3737
#' @importFrom stringr str_split
3838
#' @export
39-
get_substitutions = function(
39+
get_substitutions <- function(
4040
sequence_1,
4141
sequence_2,
4242
position_map = 1:nchar(sequence_1),
@@ -58,12 +58,12 @@ get_substitutions = function(
5858
}
5959
}
6060

61-
lengths = nchar(c(sequence_1, sequence_2))
61+
lengths <- nchar(c(sequence_1, sequence_2))
6262

6363
if (!all(lengths == min(lengths))) {
6464
message('Trimming to shortest length: ', min(lengths))
65-
sequence_1 = substr(sequence_1, 1, min(lengths))
66-
sequence_2 = substr(sequence_2, 1, min(lengths))
65+
sequence_1 <- substr(sequence_1, 1, min(lengths))
66+
sequence_2 <- substr(sequence_2, 1, min(lengths))
6767
}
6868

6969
if (length(sequence_1) == 1 & length(sequence_2) == 1 & simplify) {
@@ -75,21 +75,21 @@ get_substitutions = function(
7575
))
7676
}
7777

78-
sequence_2_uniques = unique(sequence_2)
79-
idxs = match(sequence_2, sequence_2_uniques)
78+
sequence_2_uniques <- unique(sequence_2)
79+
idxs <- match(sequence_2, sequence_2_uniques)
8080

8181
# Store original names from sequence_2
8282
sequence_2_names <- names(sequence_2)
8383
has_names <- !is.null(sequence_2_names) && !all(is.na(sequence_2_names))
8484

85-
unique_substitutions = get_substitutions_CASE_MULTIPLE(
85+
unique_substitutions <- get_substitutions_CASE_MULTIPLE(
8686
sequence_1,
8787
sequence_2_uniques,
8888
position_map,
8989
exclude
9090
)
9191

92-
all_substitutions = unique_substitutions[idxs]
92+
all_substitutions <- unique_substitutions[idxs]
9393

9494
# Preserve names from sequence_2 in output (only for multi-sequence case)
9595
if (has_names) {
@@ -100,13 +100,13 @@ get_substitutions = function(
100100
}
101101

102102

103-
get_substitutions_CASE_SINGLE = function(
103+
get_substitutions_CASE_SINGLE <- function(
104104
sequence_1,
105105
sequence_2,
106106
position_map = 1:max(nchar(sequence_1), nchar(sequence_2)),
107107
exclude = c()
108108
) {
109-
min_len = min(
109+
min_len <- min(
110110
stringr::str_length(sequence_1),
111111
stringr::str_length(sequence_2)
112112
)
@@ -122,7 +122,7 @@ get_substitutions_CASE_SINGLE = function(
122122
sequence_1_split <- stringr::str_split(sequence_1, '')[[1]][1:min_len]
123123
sequence_2_split <- stringr::str_split(sequence_2, '')[[1]][1:min_len]
124124

125-
diffs = unlist(
125+
diffs <- unlist(
126126
purrr::pmap(
127127
list(
128128
s1 = sequence_1_split,
@@ -139,14 +139,14 @@ get_substitutions_CASE_SINGLE = function(
139139

140140
# Convert NULL to character(0) for consistent return type
141141
if (is.null(diffs)) {
142-
diffs = character(0)
142+
diffs <- character(0)
143143
}
144144

145145
diffs
146146
}
147147

148148

149-
get_substitutions_CASE_MULTIPLE = function(
149+
get_substitutions_CASE_MULTIPLE <- function(
150150
sequence_1,
151151
sequence_2,
152152
position_map = 1:nchar(sequence_1),
@@ -157,17 +157,17 @@ get_substitutions_CASE_MULTIPLE = function(
157157
return(rep(list(character(0)), length(sequence_2)))
158158
}
159159

160-
diffs = rep(list(NULL), length(sequence_2))
160+
diffs <- rep(list(NULL), length(sequence_2))
161161

162-
sequence_1_split = stringr::str_split(sequence_1, pattern = "")[[1]]
163-
sequence_2_split = Biostrings::AAStringSet(sequence_2)
162+
sequence_1_split <- stringr::str_split(sequence_1, pattern = "")[[1]]
163+
sequence_2_split <- Biostrings::AAStringSet(sequence_2)
164164

165165
for (i in seq_along(sequence_1_split)) {
166-
sequence_2_pos_i = as.character(Biostrings::subseq(sequence_2_split, i, i))
167-
incl = sequence_1_split[[i]] != sequence_2_pos_i &
166+
sequence_2_pos_i <- as.character(Biostrings::subseq(sequence_2_split, i, i))
167+
incl <- sequence_1_split[[i]] != sequence_2_pos_i &
168168
(!(sequence_2_pos_i %in% exclude | sequence_1_split[[i]] %in% exclude))
169169

170-
diffs[incl] = purrr::map2(
170+
diffs[incl] <- purrr::map2(
171171
diffs[incl],
172172
paste0(
173173
sequence_1_split[[i]],
@@ -179,7 +179,7 @@ get_substitutions_CASE_MULTIPLE = function(
179179
}
180180

181181
# Convert NULL to character(0) for consistent return type
182-
diffs = lapply(diffs, function(x) {
182+
diffs <- lapply(diffs, function(x) {
183183
if (is.null(x)) character(0) else x
184184
})
185185

R/mafft_align.R

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
#'
2323
#' @note Requires MAFFT to be installed and available in PATH
2424
#' @export
25-
mafft_align = function(unaligned_sequences, reference_sequence, noisy = F) {
25+
mafft_align <- function(unaligned_sequences, reference_sequence, noisy = F) {
2626
# Input validation
2727
if (length(reference_sequence) != 1) {
2828
stop(
@@ -36,28 +36,28 @@ mafft_align = function(unaligned_sequences, reference_sequence, noisy = F) {
3636
stop("MAFFT is not installed or not available in PATH")
3737
}
3838

39-
NA_sequence_locations = is.na(unaligned_sequences)
39+
NA_sequence_locations <- is.na(unaligned_sequences)
4040

41-
temp_mafft_folder = tempdir()
41+
temp_mafft_folder <- tempdir()
4242

43-
reference_seq_file = tempfile(
43+
reference_seq_file <- tempfile(
4444
pattern = "reference",
4545
fileext = ".fasta",
4646
tmpdir = temp_mafft_folder
4747
)
48-
unaligned_seqs_file = tempfile(
48+
unaligned_seqs_file <- tempfile(
4949
pattern = "sequences",
5050
fileext = ".fasta",
5151
tmpdir = temp_mafft_folder
5252
)
53-
aligned_seqs_file = tempfile(
53+
aligned_seqs_file <- tempfile(
5454
pattern = "aligned",
5555
fileext = ".fasta",
5656
tmpdir = temp_mafft_folder
5757
)
5858

59-
unique_unaligned_sequences = unique(unaligned_sequences)
60-
all_to_unique_sequence_map = match(
59+
unique_unaligned_sequences <- unique(unaligned_sequences)
60+
all_to_unique_sequence_map <- match(
6161
unaligned_sequences,
6262
unique_unaligned_sequences
6363
)
@@ -88,12 +88,12 @@ mafft_align = function(unaligned_sequences, reference_sequence, noisy = F) {
8888
)
8989
)
9090

91-
unique_aligned_sequences = toupper(fast_fasta(aligned_seqs_file)[-1])
92-
all_aligned_sequences = unique_aligned_sequences[all_to_unique_sequence_map]
91+
unique_aligned_sequences <- toupper(fast_fasta(aligned_seqs_file)[-1])
92+
all_aligned_sequences <- unique_aligned_sequences[all_to_unique_sequence_map]
9393

94-
all_aligned_sequences[NA_sequence_locations] = NA
94+
all_aligned_sequences[NA_sequence_locations] <- NA
9595

96-
names(all_aligned_sequences) = names(unaligned_sequences)
96+
names(all_aligned_sequences) <- names(unaligned_sequences)
9797

9898
all_aligned_sequences
9999
}

0 commit comments

Comments
 (0)