Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions inst/app/R/fcs_import.R
Original file line number Diff line number Diff line change
Expand Up @@ -328,16 +328,20 @@ filter_flow_channels <- function(ff) {
# rather than the detector — $PnN "FL1-A" with $PnS "FITC-A" — satisfies it on every
# fluorescence channel while having no detectors at all, and filtering it drops the
# height partners and any spelled-out width. Beckman CytoFLEX lost 5 of 14 channels
# this way and Xitogen XTG-1600 lost 14 of 32. So also require at least one channel
# the filter would actually drop: not scatter, not LightLoss/Autofluorescence/
# Extinction, not QC/timing, and carrying no marker of its own.
# this way and Xitogen XTG-1600 lost 14 of 32. So also require channels the filter
# would actually drop -- not scatter, not LightLoss/Autofluorescence/Extinction, not
# QC/timing, and carrying no marker of its own -- and more than one of them: a
# conventional file with a single unlabelled fluorescence channel (an unstained or
# spare detector) was taken for a spectral one, which dropped its height and width
# channels and changed every gate's channel identity between files of one panel. A
# spectral file carries dozens. GateLab's channels.ts applies the same rule.
has_marker <- !is.na(pns) & nchar(trimws(pns)) > 0 & pns != pnn
is_raw_detector <- !has_marker &
!grepl("^(FSC|SSC)", pnn, ignore.case = TRUE) &
!grepl("^(LightLoss|Autofluorescence|Extinction)", pnn, ignore.case = TRUE) &
!grepl("^(Time|Event_length|Cell_length)$", pnn, ignore.case = TRUE)

if (sum(is_unmixed) < 2 || !any(is_raw_detector)) {
if (sum(is_unmixed) < 2 || sum(is_raw_detector) < 2) {
display_name <- ifelse(is.na(pns) | nchar(trimws(pns)) == 0, pnn, pns)
message(" Conventional flow: keeping all ", length(pnn), " channels")
return(list(ff = ff, display_names = display_name,
Expand Down Expand Up @@ -382,9 +386,11 @@ filter_flow_channels <- function(ff) {
next
}

# Unmixed fluorophore channels: PnS != PnN AND PnS ends with "-A"
# Unmixed fluorophore channels: PnS != PnN AND PnS ends with "-A". Case-insensitive, as
# the detection above is: a marker written "-a" counted towards making the file unmixed
# and then fell through to the drop branch here, losing the channel.
if (!is.na(desc) && nchar(trimws(desc)) > 0 &&
desc != ch && grepl("-A$", desc)) {
desc != ch && grepl("-A$", desc, ignore.case = TRUE)) {
keep_idx[i] <- TRUE
display_name[i] <- paste0(desc, " (", ch, ")")
pnr_out[i] <- pnr[i]
Expand Down
36 changes: 30 additions & 6 deletions tests/testthat/test-channel-identities.R
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,14 @@ test_that("conjugate names ending -A do not make a file spectral-unmixed", {
unlink(path)
})

test_that("a raw detector alongside unmixed markers still triggers filtering", {
test_that("raw detectors alongside unmixed markers still trigger filtering", {
skip_if_not_installed("flowCore")

# Same file plus one raw spectral detector (no $PnS of its own). That is what an
# unmixed export looks like, so the detector is dropped and markers are renamed.
chans <- c("FSC-A", "B1-A", "V500-A", "PE-A")
descs <- c("FSC-A", NA, "CD4-A", "CD25-A")
# Same file plus raw spectral detectors (no $PnS of their own). That is what an unmixed
# export looks like, so the detectors are dropped and markers are renamed. A marker written
# "-a" is an unmixed marker too, in the keep rule as in the detection.
chans <- c("FSC-A", "B1-A", "B2-A", "V500-A", "PE-A")
descs <- c("FSC-A", NA, NA, "cd4-a", "CD25-A")
values <- matrix(seq_len(3 * length(chans)), nrow = 3,
dimnames = list(NULL, chans))
frame <- flowCore::flowFrame(values)
Expand All @@ -76,7 +77,30 @@ test_that("a raw detector alongside unmixed markers still triggers filtering", {
sce <- import_fcs_files(path, instrument_mode = "flow")

expect_false("B1-A" %in% rownames(sce))
expect_identical(rownames(sce), c("FSC-A", "CD4-A (V500-A)", "CD25-A (PE-A)"))
expect_false("B2-A" %in% rownames(sce))
expect_identical(rownames(sce), c("FSC-A", "cd4-a (V500-A)", "CD25-A (PE-A)"))

unlink(path)
})

test_that("one unlabelled fluorescence channel does not make a conventional file spectral", {
skip_if_not_installed("flowCore")

# An unstained or spare channel on a conventional analyser: one channel without $PnS. It
# used to count as the raw detector that makes a file spectral-unmixed, which dropped every
# -H channel and changed the gate channel identities between files of one panel.
chans <- c("FSC-A", "FL1-H", "FL1-A", "FL2-H", "FL2-A", "FL3-A")
descs <- c("FSC-A", "FITC-H", "FITC-A", "PE-H", "PE-A", NA)
values <- matrix(seq_len(3 * length(chans)), nrow = 3,
dimnames = list(NULL, chans))
frame <- flowCore::flowFrame(values)
flowCore::pData(flowCore::parameters(frame))$desc <- descs
path <- tempfile(fileext = ".fcs")
flowCore::write.FCS(frame, path)

sce <- import_fcs_files(path, instrument_mode = "flow")

expect_identical(rownames(sce), c("FSC-A", "FITC-H", "FITC-A", "PE-H", "PE-A", "FL3-A"))

unlink(path)
})