Skip to contents

Introduction

In spectral flow cytometry (SFC), having good quality unmixing controls (both single color and unstained) is critical to the ability to take a full-stained sample and be able to decipher on an individual cell basis what fluorophore is present and the relative amount. Each single color unmixing control is itself a combination of the staining fluorophore and the underlying autofluorescence. Once autofluorescence is subtracted, the leftover signal should be distinct enough to be differentiated from other fluorophores in the reference matrix for unmixing using ordinary least squares to be effective.

When the above assumptions are broken, we end up with uncertainty in the calculation, which becomes visible in the form of unmixing errors. However, despite the importance foundational to the final result, we have limited tools by which we can address the quality of our unmixing controls beyond trial and errors.

This specific vignette addresses the Luciernaga functions that are focused on characterizing fluorescence signatures from individual .fcs files. They work on both unstained .fcs files where no subtraction takes place, as well as on single color .fcs files, where autofluorescent background (either internally or externally-derived) is first subtracted. The outputs can be either in a data.frame format that can be used for report generation, or as purified .fcs made up of populations of cells sharing the same normalized signature.

This vignette will cover the pre-processing and report generation components of the process. For the Unmixing steps using the resulting files, please refer to the unmixing vignette. We hope this allows a better understanding of how fluorophore signature and brightness impacts the typically ordinal least squares methods that are used to unmix full-stained samples.

Getting Started

To use the following Luciernaga functions, we will be working with raw .FCS files that retain their detector information (ex. UV7-A, V7-A, B3-A, etc.). We will bring these files into GatingSets comprising both unstained and single color unmixing controls, divided by whether the reference controls are cells or beads for ease of explanation.

Install Required Packages Libraries

Luciernaga relies on the infrastructure provided by other cytometry R packages like flowCore and flowWorkspace available through Bioconductor. It also relies on tidyverse packages available via CRAN. It is important to make sure that each of the following packages is installed on your computer before beginning. If you are new to R, an example of how to do this is provided below:

# To Install a CRAN Package:
install.packages("dplyr")
install.packages("BiocManager")

# To Install a Bioconductor Package:
BiocManager::install("flowCore")

Once you have the packages installed, you make sure that their contents are accessible by calling them via library:

Locating .fcs files

To get started, you will first need to provide the location on your computer where the .fcs files of interest are being stored. An example of how the author does this is provided below and can be modified for your own user and desired computer folder.

File_Location <- file.path("C:", "Users", "JohnDoe", "Desktop",
                           "TodaysExperiment")
FCS_Pattern <- ".fcs$"
FCS_Files <- list.files(path = File_Location, pattern = FCS_Pattern,
                        full.names = TRUE, recursive = FALSE)

For this vignette, we will using several small .fcs files that are stored in Luciernaga’s extdata folder for use in our examples.

File_Location <- system.file("extdata", package = "Luciernaga")
FCS_Pattern <- ".fcs$"
FCS_Files <- list.files(path = File_Location, pattern = FCS_Pattern,
                        full.names = TRUE, recursive = FALSE)
head(FCS_Files[10:30], 20)
#>  [1] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/21_Before.fcs"             
#>  [2] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/22_After.fcs"              
#>  [3] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/22_Before.fcs"             
#>  [4] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/23_After.fcs"              
#>  [5] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/23_Before.fcs"             
#>  [6] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/4BeadsUnstained(Beads).fcs"
#>  [7] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CCR4_BUV615(Beads).fcs"    
#>  [8] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CCR4_BUV615(Cells).fcs"    
#>  [9] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CCR6_BV786(Beads).fcs"     
#> [10] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CCR6_BV786(Cells).fcs"     
#> [11] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CCR7_BV650(Beads).fcs"     
#> [12] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CCR7_BV650(Cells).fcs"     
#> [13] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CD107a_APC-R700(Beads).fcs"
#> [14] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CD107a_APC-R700(Cells).fcs"
#> [15] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CD127_BV421(Beads).fcs"    
#> [16] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CD127_BV421(Cells).fcs"    
#> [17] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CD16_APC(Beads).fcs"       
#> [18] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CD16_APC(Cells).fcs"       
#> [19] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CD161_BV480(Beads).fcs"    
#> [20] "C:/Users/12692/AppData/Local/R/win-library/4.4/Luciernaga/extdata/CD161_BV480(Cells).fcs"

As you can see, there are a lot of different .fcs files that are used for different examples for the different vignettes, we will be selectively filtering from this list to get specific files that we need.

UnstainedFCSFiles <- FCS_Files[grep("Unstained", FCS_Files)]
UnstainedBeads <- UnstainedFCSFiles[grep("Beads", UnstainedFCSFiles)]
UnstainedCells <- UnstainedFCSFiles[-grep("Beads", UnstainedFCSFiles)]

BeadFCSFiles <- FCS_Files[grep("Beads", FCS_Files)]
BeadSingleColors <- BeadFCSFiles[-grep("Unstained", BeadFCSFiles)]

CellSingleColorFiles <- FCS_Files[grep("Cells", FCS_Files)]
CellSingleColors <- CellSingleColorFiles[!str_detect("Unstained", CellSingleColorFiles)]

Creating a GatingSet Bead Single Colors

The bead single color controls are part of a 29-color SFC panel. They additionally have cell single color equivalents that we will compare to in a separate GatingSet in the next section.

Now that we have filtered the .fcs files into smaller list, we can use the flowWorkspace to bring these individual .fcs files first into a CytoSet object, then into a GatingSet object that we can add gates to:

MyBeadsCytoSet <- load_cytoset_from_fcs(BeadSingleColors,
                                        truncate_max_range = FALSE,
                                        transform = FALSE)
MyBeadsCytoSet
#> A cytoset with 30 samples.
#> 
#>   column names:
#>     Time, UV1-A, UV2-A, UV3-A, UV4-A, UV5-A, UV6-A, UV7-A, UV8-A, UV9-A, UV10-A, UV11-A, UV12-A, UV13-A, UV14-A, UV15-A, UV16-A, SSC-W, SSC-H, SSC-A, V1-A, V2-A, V3-A, V4-A, V5-A, V6-A, V7-A, V8-A, V9-A, V10-A, V11-A, V12-A, V13-A, V14-A, V15-A, V16-A, FSC-W, FSC-H, FSC-A, SSC-B-W, SSC-B-H, SSC-B-A, B1-A, B2-A, B3-A, B4-A, B5-A, B6-A, B7-A, B8-A, B9-A, B10-A, B11-A, B12-A, B13-A, B14-A, YG1-A, YG2-A, YG3-A, YG4-A, YG5-A, YG6-A, YG7-A, YG8-A, YG9-A, YG10-A, R1-A, R2-A, R3-A, R4-A, R5-A, R6-A, R7-A, R8-A
MyBeadsGatingSet <- GatingSet(MyBeadsCytoSet)
MyBeadsGatingSet
#> A GatingSet with 30 samples

For this example, we will use the openCyto package to automatically gate each of our .fcs files for the bead singlets. To do this, we first read in the example .csv file containing our desired gates (that can be found in Luciernaga’s extdata folder) using the data.table package

FileLocation <- system.file("extdata", package = "Luciernaga")
MyBeadsGates <- fread(file.path(path = FileLocation, 
                                pattern = 'GatesBeads.csv'))
gt(MyBeadsGates)
alias pop parent dims gating_method gating_args collapseDataForGating groupBy preprocessing_method preprocessing_args
nonDebris - root FSC-A gate_mindensity gate_range=c(6e5, 7e5) FALSE NA NA NA
beads + nonDebris FSC-A, SSC-A flowClust K=2, target=c(5e4, 1e5) FALSE NA NA NA
singlets + beads FSC-A,FSC-H singletGate FALSE NA NA NA

For your own experiments, individual gates can be added, removed or modified to match the requirements of your own .fcs files, for additional details, please refer to the openCyto packages vignettes. Alternatively, GatingSet objects can be brought directly from several commercial software formats using the CytoML package.

Now that we have the gating information from the .csv file, we can convert them into a GatingTemplate, and append them to the .fcs files contained within the GatingSet

MyBeadsGatingTemplate <- gatingTemplate(MyBeadsGates)
gt_gating(MyBeadsGatingTemplate, MyBeadsGatingSet)
MyBeadsGatingSet[[1]]
#> Sample:  CCR4_BUV615(Beads).fcs 
#> GatingHierarchy with  4  gates
plot(MyBeadsGatingSet)

And finally check the gate placements using the Utility_GatingPlots function:

removestrings <-  c("(Cells)", ".fcs", " ")
StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")

IndividualBeadPlot <- Utility_GatingPlots(x=MyBeadsGatingSet[[1]], 
                                          sample.name = "GUID",
                                          removestrings = removestrings,
                                          gtFile = MyBeadsGates, 
                                          DesiredGates = NULL,
                                          outpath = StorageLocation,
                                          export = FALSE)

IndividualBeadPlot
#> $`1`

IteratedBeadPlots <- map(.x = MyBeadsGatingSet[1:3], .f = Utility_GatingPlots,
                         sample.name = "GUID", removestrings = removestrings,
                         gtFile = MyBeadsGates, DesiredGates = NULL,
                         outpath = StorageLocation, export = FALSE)
IteratedBeadPlots
#> [[1]]
#> [[1]]$`1`

#> 
#> 
#> [[2]]
#> [[2]]$`1`

#> 
#> 
#> [[3]]
#> [[3]]$`1`



#gs_pop_get_count_fast(MyBeadsGatingSet)

Creating a GatingSet Cell Single Colors

These cell single color controls are part of 29-color SFC panel, they are the cell counterparts to the bead reference controls we worked with in the prior section. Having isolated the list of desired .fcs files at the start of the vignette, we can use the flowWorkspace to bring these individual .fcs files first into a CytoSet object, then into a GatingSet object that we can add gates to:

MyCytoSet <- load_cytoset_from_fcs(CellSingleColors, 
                                   truncate_max_range = FALSE, 
                                   transform = FALSE)
MyCytoSet
#> A cytoset with 30 samples.
#> 
#>   column names:
#>     Time, UV1-A, UV2-A, UV3-A, UV4-A, UV5-A, UV6-A, UV7-A, UV8-A, UV9-A, UV10-A, UV11-A, UV12-A, UV13-A, UV14-A, UV15-A, UV16-A, SSC-W, SSC-H, SSC-A, V1-A, V2-A, V3-A, V4-A, V5-A, V6-A, V7-A, V8-A, V9-A, V10-A, V11-A, V12-A, V13-A, V14-A, V15-A, V16-A, FSC-W, FSC-H, FSC-A, SSC-B-W, SSC-B-H, SSC-B-A, B1-A, B2-A, B3-A, B4-A, B5-A, B6-A, B7-A, B8-A, B9-A, B10-A, B11-A, B12-A, B13-A, B14-A, YG1-A, YG2-A, YG3-A, YG4-A, YG5-A, YG6-A, YG7-A, YG8-A, YG9-A, YG10-A, R1-A, R2-A, R3-A, R4-A, R5-A, R6-A, R7-A, R8-A
MyGatingSet <- GatingSet(MyCytoSet)
MyGatingSet
#> A GatingSet with 30 samples

For this example, we will use the openCyto package to automatically gate each of our .fcs files for the lymphocyte population. To do this, we first read in the example .csv file containing our desired gates (that can be found in Luciernaga’s extdata folder) using the data.table package

FileLocation <- system.file("extdata", package = "Luciernaga")
MyGates <- fread(file.path(path = FileLocation, pattern = 'Gates.csv'))
gt(MyGates)
alias pop parent dims gating_method gating_args collapseDataForGating groupBy preprocessing_method preprocessing_args
singletsFSC + root FSC-A,FSC-H singletGate FALSE NA NA NA
singletsSSC + singletsFSC SSC-A,SSC-H singletGate FALSE NA NA NA
singletsSSCB + singletsSSC SSC-A,SSC-B-A singletGate FALSE NA NA NA
nonDebris + singletsSSCB FSC-A gate_mindensity FALSE NA NA NA
lymphocytes + nonDebris FSC-A, SSC-A flowClust K=2, target=c(1e5, 5e4) FALSE NA NA NA

For your own experiments, individual gates can be added, removed or modified to match the requirements of your own .fcs files, for additional details, please refer to the openCyto packages vignettes. Alternatively, GatingSet objects can be brought directly from several commercial software formats using the CytoML package.

Now that we have the gating information from the .csv file, we can convert them into a GatingTemplate, and append them to the .fcs files contained within the GatingSet

MyGatingTemplate <- gatingTemplate(MyGates)
gt_gating(MyGatingTemplate, MyGatingSet)
MyGatingSet[[1]]
#> Sample:  CCR4_BUV615(Cells).fcs 
#> GatingHierarchy with  6  gates

And finally check the gate placements using:

removestrings <-  c("(Cells)", ".fcs", " ")
StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")

IndividualPlot <- Utility_GatingPlots(x=MyGatingSet[[2]], 
                                      sample.name = "GUID", 
                                      removestrings = removestrings, 
                                      gtFile = MyGates, 
                                      DesiredGates = NULL, 
                                      outpath = StorageLocation, 
                                      export = FALSE)

#IndividualPlot
  
IteratedPlots <- map(.x = MyGatingSet[1:2], .f = Utility_GatingPlots, 
                     sample.name = "GUID", removestrings = removestrings,
                     gtFile = MyGates, DesiredGates = NULL,
                     outpath = StorageLocation, export = FALSE)

IteratedPlots[[2]]
#> $`1`

#> 
#> $`2`

Creating a GatingSet Bead Unstained

This unstained Bead cell control is by itself and could have been included in the Bead Single Color Gating Set, but we are including it in its own GatingSet for ease-of-locating reasons only. Now that we have filtered the .fcs file of interest, we can use the flowWorkspace to bring these individual .fcs files first into a CytoSet object, then into a GatingSet object that we can add gates to:

MyUnstainedBeadsCytoSet <- load_cytoset_from_fcs(UnstainedBeads,
                                        truncate_max_range = FALSE,
                                        transform = FALSE)
MyUnstainedBeadsCytoSet
#> A cytoset with 1 samples.
#> 
#>   column names:
#>     Time, UV1-A, UV2-A, UV3-A, UV4-A, UV5-A, UV6-A, UV7-A, UV8-A, UV9-A, UV10-A, UV11-A, UV12-A, UV13-A, UV14-A, UV15-A, UV16-A, SSC-W, SSC-H, SSC-A, V1-A, V2-A, V3-A, V4-A, V5-A, V6-A, V7-A, V8-A, V9-A, V10-A, V11-A, V12-A, V13-A, V14-A, V15-A, V16-A, FSC-W, FSC-H, FSC-A, SSC-B-W, SSC-B-H, SSC-B-A, B1-A, B2-A, B3-A, B4-A, B5-A, B6-A, B7-A, B8-A, B9-A, B10-A, B11-A, B12-A, B13-A, B14-A, YG1-A, YG2-A, YG3-A, YG4-A, YG5-A, YG6-A, YG7-A, YG8-A, YG9-A, YG10-A, R1-A, R2-A, R3-A, R4-A, R5-A, R6-A, R7-A, R8-A
MyUnstainedBeadsGatingSet <- GatingSet(MyUnstainedBeadsCytoSet)
MyUnstainedBeadsGatingSet
#> A GatingSet with 1 samples

For this example, we will use the openCyto package to automatically gate each of our .fcs files for the bead singlets. To do this, we first read in the example .csv file containing our desired gates (that can be found in Luciernaga’s extdata folder) using the data.table package

# Failing here?
FileLocation <- system.file("extdata", package = "Luciernaga")
MyBeadsGates <- fread(file.path(path = FileLocation, 
                                pattern = 'GatesBeads.csv'))
gt(MyBeadsGates)
alias pop parent dims gating_method gating_args collapseDataForGating groupBy preprocessing_method preprocessing_args
nonDebris - root FSC-A gate_mindensity gate_range=c(6e5, 7e5) FALSE NA NA NA
beads + nonDebris FSC-A, SSC-A flowClust K=2, target=c(5e4, 1e5) FALSE NA NA NA
singlets + beads FSC-A,FSC-H singletGate FALSE NA NA NA

For your own experiments, individual gates can be added, removed or modified to match the requirements of your own .fcs files, for additional details, please refer to the openCyto packages vignettes. Alternatively, GatingSet objects can be brought directly from several commercial software formats using the CytoML package.

Now that we have the gating information from the .csv file, we can convert them into a GatingTemplate, and append them to the .fcs files contained within the GatingSet

MyBeadsGatingTemplate <- gatingTemplate(MyBeadsGates)
gt_gating(MyBeadsGatingTemplate, MyUnstainedBeadsGatingSet)
MyUnstainedBeadsGatingSet[[1]]
#> Sample:  4BeadsUnstained(Beads).fcs 
#> GatingHierarchy with  4  gates
plot(MyBeadsGatingSet)

And finally check the gate placements using the Utility_GatingPlots function:

removestrings <-  c("(Cells)", ".fcs", " ")
StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")

IndividualBeadPlot <- Utility_GatingPlots(x=MyUnstainedBeadsGatingSet[[1]], 
                                          sample.name = "GUID",
                                          removestrings = removestrings,
                                          gtFile = MyBeadsGates, 
                                          DesiredGates = NULL,
                                          outpath = StorageLocation,
                                          export = FALSE)

IndividualBeadPlot
#> $`1`

Creating a GatingSet Cell Unstained Controls

These unstained cell controls were collected for CBMC and PBMC specimens, and stimulated with either PMA-ionomycin or control. They are useful to evaluate heterogeneity present in autofluorescence across donors and treatment conditions. Having isolated the list of desired .fcs files at the start of the vignette, we can use the flowWorkspace to bring these individual .fcs files first into a CytoSet object, then into a GatingSet object that we can add gates to:

MyUnstainedCytoSet <- load_cytoset_from_fcs(UnstainedCells, 
                                   truncate_max_range = FALSE, 
                                   transform = FALSE)
MyUnstainedCytoSet
#> A cytoset with 18 samples.
#> 
#>   column names:
#>     Time, UV1-A, UV2-A, UV3-A, UV4-A, UV5-A, UV6-A, UV7-A, UV8-A, UV9-A, UV10-A, UV11-A, UV12-A, UV13-A, UV14-A, UV15-A, UV16-A, SSC-W, SSC-H, SSC-A, V1-A, V2-A, V3-A, V4-A, V5-A, V6-A, V7-A, V8-A, V9-A, V10-A, V11-A, V12-A, V13-A, V14-A, V15-A, V16-A, FSC-W, FSC-H, FSC-A, SSC-B-W, SSC-B-H, SSC-B-A, B1-A, B2-A, B3-A, B4-A, B5-A, B6-A, B7-A, B8-A, B9-A, B10-A, B11-A, B12-A, B13-A, B14-A, YG1-A, YG2-A, YG3-A, YG4-A, YG5-A, YG6-A, YG7-A, YG8-A, YG9-A, YG10-A, R1-A, R2-A, R3-A, R4-A, R5-A, R6-A, R7-A, R8-A
MyUnstainedGatingSet <- GatingSet(MyUnstainedCytoSet)
MyUnstainedGatingSet
#> A GatingSet with 18 samples

For this example, we will use the openCyto package to automatically gate each of our .fcs files for the lymphocyte population. To do this, we first read in the example .csv file containing our desired gates (that can be found in Luciernaga’s extdata folder) using the data.table package

FileLocation <- system.file("extdata", package = "Luciernaga")
MyGates <- fread(file.path(path = FileLocation, pattern = 'Gates.csv'))
gt(MyGates)
alias pop parent dims gating_method gating_args collapseDataForGating groupBy preprocessing_method preprocessing_args
singletsFSC + root FSC-A,FSC-H singletGate FALSE NA NA NA
singletsSSC + singletsFSC SSC-A,SSC-H singletGate FALSE NA NA NA
singletsSSCB + singletsSSC SSC-A,SSC-B-A singletGate FALSE NA NA NA
nonDebris + singletsSSCB FSC-A gate_mindensity FALSE NA NA NA
lymphocytes + nonDebris FSC-A, SSC-A flowClust K=2, target=c(1e5, 5e4) FALSE NA NA NA

For your own experiments, individual gates can be added, removed or modified to match the requirements of your own .fcs files, for additional details, please refer to the openCyto packages vignettes. Alternatively, GatingSet objects can be brought directly from several commercial software formats using the CytoML package.

Now that we have the gating information from the .csv file, we can convert them into a GatingTemplate, and append them to the .fcs files contained within the GatingSet

MyGatingTemplate <- gatingTemplate(MyGates)
gt_gating(MyGatingTemplate, MyUnstainedGatingSet)
MyUnstainedGatingSet[[1]]
#> Sample:  INF071_Ctrl_Unstained.fcs 
#> GatingHierarchy with  6  gates

And finally check the gate placements using:

removestrings <-  c("(Cells)", ".fcs", " ")
StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")

IndividualPlot <- Utility_GatingPlots(x=MyUnstainedGatingSet[[2]], 
                                      sample.name = "GUID", 
                                      removestrings = removestrings, 
                                      gtFile = MyGates, 
                                      DesiredGates = NULL, 
                                      outpath = StorageLocation, 
                                      export = FALSE)

#IndividualPlot
  
IteratedPlots <- map(.x = MyUnstainedGatingSet[1:2], .f = Utility_GatingPlots, 
                     sample.name = "GUID", removestrings = removestrings,
                     gtFile = MyGates, DesiredGates = NULL,
                     outpath = StorageLocation, export = FALSE)

IteratedPlots[[1]]
#> $`1`

#> 
#> $`2`

AveragedSignature and QC_ViewSignature

Before moving on to the more-automated functions, let’s demonstrate the underlying building blocks that we use to evaluate fluorescent signatures within .fcs files. Within each .fcs file, the information related to individual cells measurements is stored within exprs. Let’s demonstrate an example of how this is retrieved.

PopulationInterest <- gs_pop_get_data(MyUnstainedGatingSet[1], subset="lymphocytes")
TheDataValues <- exprs(PopulationInterest[[1]])
TheDataValues <- data.frame(TheDataValues, check.names=FALSE)
head(TheDataValues, 5)
#>     Time    UV1-A     UV2-A     UV3-A     UV4-A     UV5-A    UV6-A    UV7-A
#> 1 100991 287.0875  865.6507  330.3738  894.5082 1029.7963 1117.410 2144.380
#> 2 518430 797.0026   21.7175  310.1438  774.5413  620.4363 1101.345 2400.379
#> 3 363287 366.2225  816.4144  561.6057 1122.9882  514.0800 1031.061 1086.768
#> 4 128030 866.3944 1672.2476 1169.3982 1018.8632 1989.2339 2996.271 5853.759
#> 5 201752 377.4532  640.0713 -152.3200  759.2944  688.4894 1166.572 2252.224
#>      UV8-A    UV9-A    UV10-A    UV11-A    UV12-A    UV13-A    UV14-A    UV15-A
#> 1 1890.613 1469.427  712.1407  395.3031 512.89001  79.20938 658.73938 470.57065
#> 2 2252.744 1558.603  365.1069  478.6031 -36.74125 360.12378 974.90753 682.24188
#> 3 1645.993 1502.375  692.5800  460.8275 827.19879 376.56064  26.10563 561.23376
#> 4 4622.258 4908.527 2213.4001 2142.3721 558.18439 690.42316 779.30127 832.62817
#> 5 1932.486 2221.953  731.1807  556.4738 207.06001  95.79501 341.67877  70.13563
#>      UV16-A    SSC-W   SSC-H     SSC-A     V1-A      V2-A     V3-A     V4-A
#> 1  340.3400 658951.4 1100375 1208489.5  22.9625  815.7875 1472.144 1973.675
#> 2 -172.0294 663280.7 1027170 1135503.4 345.4000 1093.1250 1360.219 1895.506
#> 3  253.7675 637444.7  867507  921646.2 730.1938 1326.6688 1440.244 1610.675
#> 4  447.5888 834948.1 1027346 1429634.4 926.5438 3792.5938 5305.712 6203.244
#> 5 -226.6206 629706.4  641798  673573.9 270.3250  986.9750 2229.494 2461.250
#>        V5-A      V6-A      V7-A      V8-A      V9-A     V10-A     V11-A
#> 1  3333.137  2550.419  3789.913  3033.387  2055.969  2121.969 1218.3876
#> 2  3019.500  2855.188  3932.294  2555.438  1841.056  2300.306  755.0125
#> 3  3323.856  2341.625  3614.394  3397.625  1903.825  2800.738 1258.3313
#> 4 10569.969 12557.257 19422.701 16951.000 11412.913 13348.706 7723.7876
#> 5  4007.644  4480.644  7203.006  5535.200  4504.844  4350.294 2635.3938
#>       V12-A     V13-A     V14-A     V15-A    V16-A    FSC-W   FSC-H     FSC-A
#> 1  781.6188  915.5438  763.6063  387.2688 428.3125 650326.3 1277333 1384472.1
#> 2  737.2750  746.4188  478.1562   81.4000 478.9813 672487.3 1353167 1516646.0
#> 3  601.3563  514.2500  706.2000 -155.9250 739.6125 681057.6 1142994 1297407.9
#> 4 3604.2876 3363.9375 3308.4563 2351.1125 825.0000 855744.7  488498  696715.9
#> 5 1387.0312 1101.8563  926.2000  860.2688 612.7000 664640.4  322764  357536.7
#>    SSC-B-W SSC-B-H  SSC-B-A      B1-A      B2-A     B3-A      B4-A     B5-A
#> 1 665813.2  653211 724860.8 1035.5363 1193.3193 1382.389  868.2256  559.290
#> 2 643891.6  771916 828383.8  665.0581  900.7994 1516.739  871.5087 1002.641
#> 3 636927.8  485970 515879.7 1696.0237 1306.6837 1446.635  727.8881 1036.244
#> 4 814962.2  564298 766469.2 3459.2549 4499.4263 6746.500 6100.3037 5927.199
#> 5 616127.5  434878 446567.2 1359.9219 1967.3644 2749.649 1878.0118 1274.496
#>        B6-A      B7-A      B8-A      B9-A     B10-A     B11-A     B12-A
#> 1  429.4456  639.1150 -131.9044  237.9944   29.3550  324.6431  325.6731
#> 2  591.1556  343.9556  271.9200  328.4413  130.2306  557.4875  737.8019
#> 3  818.8500  940.7119  597.6575  435.0462  440.6469  282.4131  538.9475
#> 4 4646.1367 4256.9253 2672.2705 2430.0273 1379.2344 1110.1469 1117.3568
#> 5 1657.9780 1462.0206 1209.3488 1007.1469  758.9169  263.2294  262.6500
#>      B13-A     B14-A    YG1-A     YG2-A     YG3-A      YG4-A     YG5-A
#> 1 159.3925  278.3575 481.6706 -197.5106  357.2812  835.41376  406.6069
#> 2 541.0718  201.4937 191.1975  377.8163  569.2219   54.73688 -166.6387
#> 3  26.5225  172.3319 207.7781  378.2325  899.3775  493.46439  761.1825
#> 4 269.8600 1497.0405 689.5875 1649.0438 1963.6594 1809.23059 1411.7119
#> 5 275.0100  663.3200 645.1181  855.1856  361.4438 1050.26819  824.9381
#>        YG6-A      YG7-A      YG8-A     YG9-A     YG10-A      R1-A      R2-A
#> 1  544.94061  415.97250  372.19687 -444.4856 -415.00125 -139.5550  414.2863
#> 2  257.10376   46.20375  -64.51875   33.0225 -194.38875  106.4319 -182.4950
#> 3   38.78062  776.16748 -123.69563  183.7050   91.08938  271.9769  312.4450
#> 4 1519.86755 2322.88306  723.02625 1515.4969  675.92065  640.2863  672.3500
#> 5  557.91376 1073.99438   96.77813  367.8956  384.12939  294.2944  594.5919
#>        R3-A      R4-A       R5-A       R6-A       R7-A     R8-A
#> 1 324.66312  418.5944   16.03188   65.11625  248.52937 316.1881
#> 2 188.92188  370.5694  154.66875 -104.24250 -227.27126 313.2925
#> 3 374.59500 -208.1319  258.48749  421.84311  -28.32063  37.0075
#> 4 932.10876  574.1106 1004.28748  792.20062  594.52124 258.0638
#> 5  16.45563  217.3837 -164.62688  813.59998  255.66251 360.3287
TotalLymphocytes <- nrow(TheDataValues)
TotalLymphocytes
#> [1] 10000

AveragedSignature() is the Luciernaga function that takes these data outputs and for each detector summarizes them for a given statistic given in the stats argument. The main ones used are “median” and “mean”.

TheSummary <- AveragedSignature(x=TheDataValues, stats="median")
gt(TheSummary)
Time UV1-A UV2-A UV3-A UV4-A UV5-A UV6-A UV7-A UV8-A UV9-A UV10-A UV11-A UV12-A UV13-A UV14-A UV15-A UV16-A SSC-W SSC-H SSC-A V1-A V2-A V3-A V4-A V5-A V6-A V7-A V8-A V9-A V10-A V11-A V12-A V13-A V14-A V15-A V16-A FSC-W FSC-H FSC-A SSC-B-W SSC-B-H SSC-B-A B1-A B2-A B3-A B4-A B5-A B6-A B7-A B8-A B9-A B10-A B11-A B12-A B13-A B14-A YG1-A YG2-A YG3-A YG4-A YG5-A YG6-A YG7-A YG8-A YG9-A YG10-A R1-A R2-A R3-A R4-A R5-A R6-A R7-A R8-A
280707 395.4891 664.1316 492.0278 605.7844 829.021 1304.872 2519.788 1888.047 1819.882 721.921 494.631 307.1688 248.4125 303.1525 225.1703 201.8166 695970.8 1169318 1359648 444.8813 1294.803 1981.547 2249.5 3483.838 3387.141 4656.06 3545.472 2429.041 2698.369 1515.284 874.6031 799.7344 715.0344 632.7062 360.1812 680531.5 1289974 1467994 680777 790005.5 901305.7 1338.549 1547.865 2142.496 1523.724 1314.183 1063.25 783.7012 595.4687 609.9531 399.8331 311.4462 286.5009 210.3453 250.1612 608.8003 507.2006 529.2966 543.4838 422.7019 421.8 515.6297 257.7628 246.4547 166.3266 267.6687 327.4528 333.7384 355.95 219.8556 185.885 174.5497 110.3516

As you can see, there is still some non-signature information present in the form of the Time, FSC and SSC columns. After we remove these, we can pass the values corresponding to the signature to QC_ViewSignature()

TheData <- TheSummary[,-grep("Time|FS|SC|SS|Original|W$|H$", names(TheSummary))]
gt(TheData)
UV1-A UV2-A UV3-A UV4-A UV5-A UV6-A UV7-A UV8-A UV9-A UV10-A UV11-A UV12-A UV13-A UV14-A UV15-A UV16-A V1-A V2-A V3-A V4-A V5-A V6-A V7-A V8-A V9-A V10-A V11-A V12-A V13-A V14-A V15-A V16-A B1-A B2-A B3-A B4-A B5-A B6-A B7-A B8-A B9-A B10-A B11-A B12-A B13-A B14-A YG1-A YG2-A YG3-A YG4-A YG5-A YG6-A YG7-A YG8-A YG9-A YG10-A R1-A R2-A R3-A R4-A R5-A R6-A R7-A R8-A
395.4891 664.1316 492.0278 605.7844 829.021 1304.872 2519.788 1888.047 1819.882 721.921 494.631 307.1688 248.4125 303.1525 225.1703 201.8166 444.8813 1294.803 1981.547 2249.5 3483.838 3387.141 4656.06 3545.472 2429.041 2698.369 1515.284 874.6031 799.7344 715.0344 632.7062 360.1812 1338.549 1547.865 2142.496 1523.724 1314.183 1063.25 783.7012 595.4687 609.9531 399.8331 311.4462 286.5009 210.3453 250.1612 608.8003 507.2006 529.2966 543.4838 422.7019 421.8 515.6297 257.7628 246.4547 166.3266 267.6687 327.4528 333.7384 355.95 219.8556 185.885 174.5497 110.3516

For Raw MFI:

TheData <- TheData %>% mutate(Sample="TestSignature") %>% relocate(Sample, .before=1)
Plot <- Luciernaga::QC_ViewSignature(x="TestSignature", data=TheData, Normalize=FALSE)
Plot

And after-normalizing by the peak detector.

Plot <- Luciernaga::QC_ViewSignature(x="TestSignature", data=TheData, Normalize=TRUE)
#> Normalizing Data for Signature Comparison
Plot

A lot of the more complicated Luciernaga functions build on these elements in combination with filtering decisions to characterize all the signatures present within an .fcs file. However, if you have questions that are not addressed directly by one of the more complicated functions, build on the above until you answer your own question. It’s how we all initially get started.

Luciernaga_QC

Luciernaga_QC() is the Luciernaga package workhorse function. It’s primary purpose is to take individual .fcs files, identify normalized signatures at the level of individual cells, group cells with similar signatures together and then export the data as purified signature .fcs file or as a dataframe object for use in reports. Given the various types of unmixing controls it can encounter (beads or cells, single color or unstained), it coordinates various functions behind the scenes, while retaining similar processing and arguments to allow for user-directed inputs.

As with other Luciernaga functions, Luciernaga_QC() starts from GatingSet objects. For this workflow, our overarching goal is to characterize signatures present within our single color cell reference controls. To do this, we will need to lay down some groundwork that will vary depending on your individual panel and experiment context.

Autofluorescent Overlaps

For our SFC panels, a few fluorophores (BUV496, BV510) peak signature detector overlaps directly with that of the main autofluorescent detector peaks (UV7-A, V7-A). Luciernaga_QC works on the basis of filtering single stained cells from unstained cells by their peak fluorescence detectors. To handle situations when these are identical, Luciernaga_QC() refers to an autofluorescent overlap list to appropriately handle fluorophores where this is the case.

Since these will vary by panel and cell type in user-specific ways, to appropriately extract the background autofluorescence from these particular single colors, we need to provide information about where to expect to overlapping autofluorescent with fluorophore signatures in a .csv file.

FileLocation <- system.file("extdata", package = "Luciernaga")
pattern = "AutofluorescentOverlaps.csv"
AFOverlap <- list.files(path=FileLocation, pattern=pattern,
                        full.names = TRUE)
AFOverlap_CSV <- read.csv(AFOverlap, check.names = FALSE)
AFOverlap_CSV
#>   Fluorophore         MainDetector
#> 1   Unstained UV7-A,V7-A,V3-A,V5-A
#> 2      BUV496                UV7-A
#> 3       BV480                 V5-A
#> 4       BV510                 V7-A
#> 5       BV570                 V8-A

As you can see from the above example, we provide for Fluorophore “Unstained” the location of the peak detectors that autofluorescence (both the main autofluorescence (UV7-A, V7-A) and rarer variants (V5-A, V7-A, V8-A)) can be found on. If the rarer variants are unknown, list the main Autofluorescent detector in your system and add the exceptions to the .csv file as you encounter them in actual practice.

The second component are individual fluorophores that overlap with the mentioned Unstained detectors. You can add/remove to this list, the crucial part is to retain the Unstained row.

Now that an initial overlap .csv has been provided, the initial steps in our workflow focus on refining what autofluorescence backgrounds we are working with. This is important not only for isolating signatures of the single-color controls, but also for handling overlap exceptions (mentioned above) as well as determining any autofluorescence(s) that might be present within individual samples that will need to be treated as individual fluorophores.

To do this without running all Luciernaga_QC() extraction protocols, we first set the arguments SignatureReturnNow to TRUE. We can additionally set Verbose = TRUE to get processing information that can be useful in troubleshooting.

FileLocation <- system.file("extdata", package = "Luciernaga")
pattern = "AutofluorescentOverlaps.csv"
AFOverlap <- list.files(path=FileLocation, pattern=pattern,
                        full.names = TRUE)

removestrings <- ".fcs"

UnstainedSignature <- Luciernaga_QC(x=MyUnstainedGatingSet[2],
                                    subsets="lymphocytes", 
                                    removestrings=removestrings,
                                    sample.name="GUID",
                                    unmixingcontroltype = "cells",
                                    Unstained = TRUE, 
                                    ratiopopcutoff = 0.001,
                                    Verbose = TRUE,
                                    AFOverlap = AFOverlap,
                                    stats = "median",
                                    ExportType = "data.frame",
                                    SignatureReturnNow = TRUE,
                                    outpath = NULL)
#> 0.09 of all events were negative and will be rounded to 0
#>   Fluors Counts
#> 1     V7   3920
#> 2     V8     22
#> 3     V5     17
#> 4     V6     11
#> [1] "V7" "V8" "V5" "V6"
#> Normalizing Data for Signature Comparison
#>   Fluors Counts
#> 1     V7   3920
#> 2     V8     22
#> 3     V5     17
#> 4     V6     11

From the Verbose = TRUE readouts, we can see that the main peak detector of autofluorescence in unstained lymphocytes cells in our sample is “V7” detector, with a few cells having their respective peaks for V8, V5, V6.

For the samples provided in this R package, we down-sampled unstained files to 10,000 event, so depending on total number of cells would inform whether these minor autofluorescent peaks are negligible or important enough to add to the above AutofluorescentOverlap.csv.

What we see is also influence by the ratiopopcutoff argument (set to 0.001 above). It is used to determine minimal number of cells a peak detector would need to have in order to be returned as a significant autofluorescence detector. We can be see this in action by making the argument more stringent with this argument (increase it to 0.1 ~ 10% total unstained cells share that peak detector):

UnstainedSignature <- Luciernaga_QC(x=MyUnstainedGatingSet[2],
                                    subsets="lymphocytes", 
                                    removestrings=removestrings,
                                    sample.name="GUID",
                                    unmixingcontroltype = "cells",
                                    Unstained = TRUE, 
                                    ratiopopcutoff = 0.1,
                                    Verbose = TRUE,
                                    AFOverlap = AFOverlap,
                                    stats = "median",
                                    ExportType = "data.frame",
                                    SignatureReturnNow = TRUE,
                                    outpath = NULL)
#> 0.09 of all events were negative and will be rounded to 0
#>   Fluors Counts
#> 1     V7   3920
#> [1] "V7"
#> Normalizing Data for Signature Comparison
#>   Fluors Counts
#> 1     V7   3920

With SignatureReturnNow = TRUE, for the main peak detector present within the sample, we get an averaged signature that is returned. The average is determined by the stats argument, which is typically either “median” or “mean”.

UnstainedSignature
#>     UV1-A    UV2-A    UV3-A    UV4-A    UV5-A    UV6-A    UV7-A    UV8-A
#> 1 401.216 682.8741 544.5366 668.5197 922.0641 1473.294 2798.136 2067.067
#>      UV9-A   UV10-A   UV11-A   UV12-A   UV13-A   UV14-A   UV15-A   UV16-A
#> 1 1938.659 729.0238 473.1738 282.4763 231.1575 289.2444 215.3528 190.4744
#>       V1-A     V2-A     V3-A     V4-A     V5-A     V6-A     V7-A     V8-A
#> 1 465.5063 1445.744 2224.922 2513.191 3835.941 3698.578 5102.797 3820.334
#>       V9-A    V10-A    V11-A    V12-A    V13-A    V14-A    V15-A    V16-A
#> 1 2592.081 2877.359 1541.306 875.6344 799.4938 704.0344 614.1437 344.5406
#>       B1-A     B2-A     B3-A     B4-A     B5-A     B6-A     B7-A     B8-A
#> 1 1403.117 1631.713 2303.724 1569.205 1344.987 1071.586 778.7122 590.5762
#>       B9-A    B10-A    B11-A    B12-A    B13-A    B14-A    YG1-A    YG2-A
#> 1 621.5084 400.5734 307.1331 287.9816 206.3541 265.9009 592.3237 513.6872
#>      YG3-A    YG4-A    YG5-A    YG6-A    YG7-A   YG8-A    YG9-A   YG10-A
#> 1 507.6169 493.4644 389.5753 391.2056 497.0025 242.535 231.8166 174.7209
#>       R1-A   R2-A     R3-A     R4-A     R5-A     R6-A     R7-A   R8-A
#> 1 199.2684 268.94 274.3781 305.5944 170.1709 156.3638 149.5838 105.09

This returned signature is what we provide as an external autofluorescence argument for either the bead or single-color controls.

In cases where we want to return the averaged signature for a specific autofluorescene or one of the minor autofluorescent peak detectors (like V8, V5 or V6) we can add the additional argument desiredAF and set it to the corresponding detector (“V8-A”):

V7 <- Luciernaga_QC(x=MyUnstainedGatingSet[2], desiredAF = "V7-A",
                    subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells",
                    Unstained = TRUE, ratiopopcutoff = 0.001, Verbose = FALSE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> Normalizing Data for Signature Comparison

V8 <- Luciernaga_QC(x=MyUnstainedGatingSet[2], desiredAF = "V8-A",
                    subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells",
                    Unstained = TRUE, ratiopopcutoff = 0.001, Verbose = FALSE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> Normalizing Data for Signature Comparison

V5 <- Luciernaga_QC(x=MyUnstainedGatingSet[2], desiredAF = "V5-A",
                    subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells",
                    Unstained = TRUE, ratiopopcutoff = 0.001, Verbose = FALSE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> Normalizing Data for Signature Comparison

Comparison <- rbind(V7, V8, V5)
gt(Comparison)
UV1-A UV2-A UV3-A UV4-A UV5-A UV6-A UV7-A UV8-A UV9-A UV10-A UV11-A UV12-A UV13-A UV14-A UV15-A UV16-A V1-A V2-A V3-A V4-A V5-A V6-A V7-A V8-A V9-A V10-A V11-A V12-A V13-A V14-A V15-A V16-A B1-A B2-A B3-A B4-A B5-A B6-A B7-A B8-A B9-A B10-A B11-A B12-A B13-A B14-A YG1-A YG2-A YG3-A YG4-A YG5-A YG6-A YG7-A YG8-A YG9-A YG10-A R1-A R2-A R3-A R4-A R5-A R6-A R7-A R8-A
401.2160 682.8741 544.5366 668.5197 922.0641 1473.2944 2798.136 2067.067 1938.659 729.0238 473.1738 282.4763 231.1575 289.2444 215.35282 190.4744 465.5063 1445.744 2224.922 2513.191 3835.941 3698.578 5102.797 3820.334 2592.081 2877.359 1541.306 875.6344 799.4938 704.0344 614.1437 344.5406 1403.1174 1631.713 2303.724 1569.205 1344.9868 1071.5862 778.7122 590.5762 621.5084 400.5734 307.1331 287.9816 206.3541 265.9009 592.3237 513.6872 507.6169 493.4644 389.5753 391.2056 497.0025 242.5350 231.8166 174.7209 199.26844 268.9400 274.3781 305.5944 170.17094 156.363754 149.5838 105.09000
242.0535 543.6069 377.8994 408.4675 579.3069 966.2057 1982.800 1380.735 1152.738 456.2163 304.2310 233.3144 172.4013 239.4503 87.42782 113.0128 273.7969 1019.356 1537.594 1757.628 2781.488 2676.644 3281.334 3409.106 1912.144 2068.241 1127.053 658.4188 661.9250 497.4750 479.5656 314.3250 941.9994 1233.715 1644.942 1132.871 897.0978 873.5044 555.0091 400.4447 405.3372 254.0881 255.5044 142.1722 180.6684 448.0178 355.9285 359.6747 486.3534 284.9231 245.9344 436.8544 299.3185 187.1391 252.7678 134.4141 71.79031 295.7422 228.1541 261.2772 81.18344 57.947811 153.5034 84.64406
382.5107 521.2944 473.1738 551.7138 672.7219 1426.7357 2137.612 1689.354 1508.325 662.2350 248.5613 279.7244 101.4475 363.5450 215.46439 290.7319 272.6625 1080.613 1693.244 2038.438 3486.106 2627.694 3357.062 2769.456 2090.963 1995.125 1288.925 737.1375 337.9750 461.6562 496.3063 286.6875 1002.3831 1372.539 1683.728 1122.958 884.8987 787.3062 610.4037 353.4831 298.8931 402.0862 171.2375 339.5137 40.1700 169.3706 429.4312 482.5725 416.6663 201.0488 350.3438 276.4594 354.7144 85.4700 287.4206 223.8038 159.47125 304.8175 151.4200 202.4819 50.85000 1.906875 0.0000 196.19624

These averaged signature readouts can also be combined with the functions covered in the QC vignette to visualize the normalized signature:

Fluorophore <- data.frame(Sample=c("V7", "V8", "V5"))
Data <- cbind(Fluorophore, Comparison)

SignatureOnly <- QC_WhatsThis(x="V7", data=Data, NumberHits = 0, returnPlots = TRUE)
#> Normalizing Data for Signature Comparison
SignatureOnly[[2]]

We can also compare it to reference fluorophore spectras to see what single colors it might overlap with and significantly impact:

Comparison <- QC_WhatsThis(x="V7", data=Data, NumberHits = 5, returnPlots = TRUE)
#> Normalizing Data for Signature Comparison
Comparison[[1]]
#>      Fluorophore ID_V7
#> 1          BV510  0.89
#> 2 LIVE DEAD Aqua  0.86
#> 3          OC515  0.84
#> 4       VioGreen  0.84
#> 5    Zombie Aqua  0.84
plotly::ggplotly(Comparison[[2]])

We will typically repeat the above workflow on various unstained specimens within our GatingSet, identify if any additional rare AF may be present, and then update our AFOverlap.csv to account for these Unstained peak detectors, and add any fluorophores that share their peak detector. Once this is done, we are ready to proceed.

The SignatureReturnNow = TRUE argument will also work for single-color unmixing controls for both bead and cell reference controls, set Verbose = TRUE to see what peak detectors are returned, set desiredAF to the detector of interest to further explore different signatures. In the case below, we use the BV786 single-color bead unstained control.

pData(MyBeadsGatingSet)
#>                                                              name
#> CCR4_BUV615(Beads).fcs                     CCR4_BUV615(Beads).fcs
#> CCR6_BV786(Beads).fcs                       CCR6_BV786(Beads).fcs
#> CCR7_BV650(Beads).fcs                       CCR7_BV650(Beads).fcs
#> CD107a_APC-R700(Beads).fcs             CD107a_APC-R700(Beads).fcs
#> CD127_BV421(Beads).fcs                     CD127_BV421(Beads).fcs
#> CD16_APC(Beads).fcs                           CD16_APC(Beads).fcs
#> CD161_BV480(Beads).fcs                     CD161_BV480(Beads).fcs
#> CD25_PE-Cy5(Beads).fcs                     CD25_PE-Cy5(Beads).fcs
#> CD26_PerCP-Cy5.5(Beads).fcs           CD26_PerCP-Cy5.5(Beads).fcs
#> CD27_APC-Fire750(Beads).fcs           CD27_APC-Fire750(Beads).fcs
#> CD3_AlexaFluor488(Beads).fcs         CD3_AlexaFluor488(Beads).fcs
#> CD3_AlexaFluor647(Beads).fcs         CD3_AlexaFluor647(Beads).fcs
#> CD3_SparkBlue550(Beads).fcs           CD3_SparkBlue550(Beads).fcs
#> CD38_APC-Fire810(Beads).fcs           CD38_APC-Fire810(Beads).fcs
#> CD4_BUV805(Beads).fcs                       CD4_BUV805(Beads).fcs
#> CD45RA_BV510(Beads).fcs                   CD45RA_BV510(Beads).fcs
#> CD56_BV605(Beads).fcs                       CD56_BV605(Beads).fcs
#> CD62L_BUV395(Beads).fcs                   CD62L_BUV395(Beads).fcs
#> CD69_BUV563(Beads).fcs                     CD69_BUV563(Beads).fcs
#> CD7_BV711(Beads).fcs                         CD7_BV711(Beads).fcs
#> CD8_BUV496(Beads).fcs                       CD8_BUV496(Beads).fcs
#> CXCR3_BUV737(Beads).fcs                   CXCR3_BUV737(Beads).fcs
#> Dump_CD14_PacificBlue(Beads).fcs Dump_CD14_PacificBlue(Beads).fcs
#> Dump_CD19_PacificBlue(Beads).fcs Dump_CD19_PacificBlue(Beads).fcs
#> IFNg_BV750(Beads).fcs                       IFNg_BV750(Beads).fcs
#> NKG2D_PE(Beads).fcs                           NKG2D_PE(Beads).fcs
#> PD1_PE-Vio770(Beads).fcs                 PD1_PE-Vio770(Beads).fcs
#> TNFa_PE-Dazzle594(Beads).fcs         TNFa_PE-Dazzle594(Beads).fcs
#> Va7.2_AlexaFluor647(Beads).fcs     Va7.2_AlexaFluor647(Beads).fcs
#> VD2_BUV661(Beads).fcs                       VD2_BUV661(Beads).fcs

BV786 <- Luciernaga_QC(x=MyBeadsGatingSet[2], desiredAF = NULL,
                    subsets="singlets", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "beads",
                    Unstained = FALSE, ratiopopcutoff = 0.001, Verbose = TRUE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> 0.19 of all events were negative and will be rounded to 0
#> Returning Peak Bead Detector Medians
#>   TheDetector   TheMedian
#> 1         V15 394556.2500
#> 2         YG1    768.0050
#> 3          V7    713.0750
#> 4         UV9    764.5007
#> 5          V5    765.1188
#> 6          V3    735.7625
#> 7          V4    719.6750
#>   Fluors Counts
#> 1    V15   1146
#> [1] "V15"
#> Normalizing Data for Signature Comparison
#>   Fluors Counts
#> 1    V15   1146

For the staining beads we use, the only bright signal is the fluorophore stained beads as seen above. Beyond that, the next occupied peak detector is dictated often by noise and the normalize signature reflects this uncertainty, as we see when we set the desiredAF to retrieve the “YG1-A” instead.

pData(MyBeadsGatingSet)
#>                                                              name
#> CCR4_BUV615(Beads).fcs                     CCR4_BUV615(Beads).fcs
#> CCR6_BV786(Beads).fcs                       CCR6_BV786(Beads).fcs
#> CCR7_BV650(Beads).fcs                       CCR7_BV650(Beads).fcs
#> CD107a_APC-R700(Beads).fcs             CD107a_APC-R700(Beads).fcs
#> CD127_BV421(Beads).fcs                     CD127_BV421(Beads).fcs
#> CD16_APC(Beads).fcs                           CD16_APC(Beads).fcs
#> CD161_BV480(Beads).fcs                     CD161_BV480(Beads).fcs
#> CD25_PE-Cy5(Beads).fcs                     CD25_PE-Cy5(Beads).fcs
#> CD26_PerCP-Cy5.5(Beads).fcs           CD26_PerCP-Cy5.5(Beads).fcs
#> CD27_APC-Fire750(Beads).fcs           CD27_APC-Fire750(Beads).fcs
#> CD3_AlexaFluor488(Beads).fcs         CD3_AlexaFluor488(Beads).fcs
#> CD3_AlexaFluor647(Beads).fcs         CD3_AlexaFluor647(Beads).fcs
#> CD3_SparkBlue550(Beads).fcs           CD3_SparkBlue550(Beads).fcs
#> CD38_APC-Fire810(Beads).fcs           CD38_APC-Fire810(Beads).fcs
#> CD4_BUV805(Beads).fcs                       CD4_BUV805(Beads).fcs
#> CD45RA_BV510(Beads).fcs                   CD45RA_BV510(Beads).fcs
#> CD56_BV605(Beads).fcs                       CD56_BV605(Beads).fcs
#> CD62L_BUV395(Beads).fcs                   CD62L_BUV395(Beads).fcs
#> CD69_BUV563(Beads).fcs                     CD69_BUV563(Beads).fcs
#> CD7_BV711(Beads).fcs                         CD7_BV711(Beads).fcs
#> CD8_BUV496(Beads).fcs                       CD8_BUV496(Beads).fcs
#> CXCR3_BUV737(Beads).fcs                   CXCR3_BUV737(Beads).fcs
#> Dump_CD14_PacificBlue(Beads).fcs Dump_CD14_PacificBlue(Beads).fcs
#> Dump_CD19_PacificBlue(Beads).fcs Dump_CD19_PacificBlue(Beads).fcs
#> IFNg_BV750(Beads).fcs                       IFNg_BV750(Beads).fcs
#> NKG2D_PE(Beads).fcs                           NKG2D_PE(Beads).fcs
#> PD1_PE-Vio770(Beads).fcs                 PD1_PE-Vio770(Beads).fcs
#> TNFa_PE-Dazzle594(Beads).fcs         TNFa_PE-Dazzle594(Beads).fcs
#> Va7.2_AlexaFluor647(Beads).fcs     Va7.2_AlexaFluor647(Beads).fcs
#> VD2_BUV661(Beads).fcs                       VD2_BUV661(Beads).fcs

NotBV786 <- Luciernaga_QC(x=MyBeadsGatingSet[2], desiredAF = "YG1-A",
                    subsets="singlets", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "beads",
                    Unstained = FALSE, ratiopopcutoff = 0.001, Verbose = TRUE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> 0.19 of all events were negative and will be rounded to 0
#> Returning Peak Bead Detector Medians
#>   TheDetector   TheMedian
#> 1         V15 394556.2500
#> 2         YG1    768.0050
#> 3          V7    713.0750
#> 4         UV9    764.5007
#> 5          V5    765.1188
#> 6          V3    735.7625
#> 7          V4    719.6750
#>   Fluors Counts
#> 1    V15   1146
#> [1] "V15"
#> Normalizing Data for Signature Comparison
#>   Fluors Counts
#> 1    V15   1146

Unstained Signatures

Having established autofluorescence background for the cells we are working with (and updated the AFOverlaps csv), we can now switch SignatureReturnNow = FALSE, and allow Luciernaga_QC() to continue down the processing pipeline. Let’s continue with Unstained cell gating set, as their signatures do not require subtraction of background like single color controls, and are easier to demonstrate Luciernaga_QC() building blocks.

From the above example, we used Luciernaga_QC() to retrieve for individual detectors that passed the ratiopopcutoff limit the averaged signature of cells that shared their respective peak detectors. We can quickly visualize both the raw fluorescence and the normalized signatures using the QC_ViewSignature() function:

SignatureOnly <- QC_ViewSignature(x=c("V7", "V8", "V5"), data=Data, Normalize = FALSE)
plotly::ggplotly(SignatureOnly)
SignatureOnly <- QC_ViewSignature(x=c("V7", "V8", "V5"), data=Data, Normalize = TRUE)
#> Normalizing Data for Signature Comparison
plotly::ggplotly(SignatureOnly)

While this approach is good to quickly characterize average signature of cells at the population level for each detector, it can miss variation present in cells, especially for non-peak detector regions. For example, if we passed the same original unstained file to Luciernaga_LinearSlices(), we could see that when we cut the population for the “V7” detector in 10% increments on the basis of MFI, that we end up some variation in the normalized signatures for the secondary peaks.

RawSlices <- Luciernaga_LinearSlices(x=MyUnstainedGatingSet[2], subset="lymphocytes",
                                  sample.name="GUID", removestrings=removestrings,
                                  stats="median", returntype="raw",
                                  probsratio=0.1, output="plot", desiredAF="V7-A")

plotly::ggplotly(RawSlices)
NormalizedSlices <- Luciernaga_LinearSlices(x=MyUnstainedGatingSet[2], subset="lymphocytes",
                                  sample.name="GUID", removestrings=removestrings,
                                  stats="median", returntype="normalized",
                                  probsratio=0.1, output="plot", desiredAF="V7-A")

plotly::ggplotly(NormalizedSlices)

This is more particularly pronounced in context of the single-color unmixing controls, for example, here is what CD16 APC looks like when filtered solely on the basis of peak-detector:

#pData(MyGatingSet[6])

APC_Example <- subset(MyGatingSet, str_detect(name, "CD16_"))

RawSlices <- Luciernaga_LinearSlices(x=APC_Example[1], subset="lymphocytes",
                                  sample.name="GUID", removestrings=removestrings,
                                  stats="median", returntype="raw",
                                  probsratio=0.1, output="plot", desiredAF="R1-A")

plotly::ggplotly(RawSlices)
#pData(MyGatingSet[6])

NormalizedSlices <- Luciernaga_LinearSlices(x=APC_Example[1], subset="lymphocytes",
                                  sample.name="GUID", removestrings=removestrings,
                                  stats="median", returntype="normalized",
                                  probsratio=0.1, output="plot", desiredAF="R1-A")

plotly::ggplotly(NormalizedSlices)

For the dimmest cells in terms of brightness (raw MFI), the resulting normalized signatures retain significant portion of autofluorescence signature that contaminates the final normalized signature. We can compare how different these are by setting the returntype to “data” and passing the rows of comparison interest to QC_WhatsThis()

NormalizedSliceData <- Luciernaga_LinearSlices(x=APC_Example[1], subset="lymphocytes",
                                  sample.name="GUID", removestrings=removestrings,
                                  stats="median", returntype="normalized",
                                  probsratio=0.1, output="data", desiredAF="R1-A")

APC_90to100 <- NormalizedSliceData %>% rename(Sample = Percentiles) %>% filter(Sample %in% "90")
APC_10to20 <- NormalizedSliceData %>% rename(Sample = Percentiles) %>% filter(Sample %in% "10")
MatchingSignature <- QC_WhatsThis(x="90", data=APC_90to100, NumberHits = 10, returnPlots = TRUE)
MatchingSignature[1]
#> [[1]]
#>            Fluorophore ID_90
#> 1                  APC  1.00
#> 2          cFluor R659  1.00
#> 3                CF633  0.96
#> 4    CellTrace Far Red  0.94
#> 5               CF640R  0.94
#> 6  Live-or-Dye 640-662  0.94
#> 7         ViaKrome 638  0.94
#> 8      Vio Bright R667  0.94
#> 9             Vio R667  0.94
#> 10   NovaFluor Red 660  0.93
plotly::ggplotly(MatchingSignature[[2]])
ContaminationSignature <- QC_WhatsThis(x="10", data=APC_10to20, NumberHits = 10, returnPlots = TRUE)
ContaminationSignature[1]
#> [[1]]
#>            Fluorophore ID_10
#> 1                  APC  0.74
#> 2          cFluor R659  0.73
#> 3      StarBright R670  0.72
#> 4                CF633  0.70
#> 5    CellTrace Far Red  0.69
#> 6               CF640R  0.67
#> 7  Live-or-Dye 640-662  0.67
#> 8    NovaFluor Red 660  0.67
#> 9         ViaKrome 638  0.67
#> 10     Vio Bright R667  0.67
plotly::ggplotly(ContaminationSignature[[2]])

In this case, the cosine value of the signature from our APC single-color went from being a near match to the reference signature, to 0.76. It’s because of these reasons, that in Luciernaga_QC after the initial peak detectors are determined, we filter cells that share this peak detector, and then determine if any additional signature peaks are present. This occurs using a rolling local maxima approach to identify these additional peaks. If the fluorophore doesn’t have any additional peaks, Luciernaga_QC() will enumerate the first peak and move on. If they are present, individual cells are evaluated by the height of the retrieved detectors corresponding to these additional peaks vs the main peak. Ie, is the additional peak 0.5 (50%) the height, or 0.1 (10%) the height of the peak detector.

Once this process has been repeated for each individual cell, the enumerations are appended to create a subcluster designation. For autofluorescence cells in our system, this would often resemble a name similar to V7_10-UV8_05-B3_04. With the primary peak coming first, with it’s relative height equaling 10 (since R doesn’t like periods to allow for a 1.0) [V7_10]. Then the second highest peak and it’s relative height compared to the peak detector [-UV7_05-], and finally the final peak in the fluorescent signature and it’s height relative to the peak detector [-B3_04]. Once these subcluster names are formed, they are used to group cells with similar autofluorescence together to be exported/reported as purified signatures.

Let’s see what this looks like in the context of our original unstained sample.

FileLocation <- system.file("extdata", package = "Luciernaga")
pattern = "AutofluorescentOverlaps.csv"
AFOverlap <- list.files(path=FileLocation, pattern=pattern,
                        full.names = TRUE)
removestrings <- c(".fcs")
StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")

Unstained_Signatures <- Luciernaga_QC(x=MyUnstainedGatingSet[2],
                                      subsets="lymphocytes",
                                      removestrings=removestrings,
                                      sample.name="GUID",
                                      unmixingcontroltype = "cells",
                                      Unstained = TRUE,
                                      ratiopopcutoff = 0.001,
                                      Verbose = FALSE,
                                      AFOverlap = AFOverlap, 
                                      stats = "median",
                                      ExportType = "data",
                                      SignatureReturnNow = FALSE,
                                      outpath = NULL,
                                      Increments=0.1,
                                      experiment = "UnstainedSignature",
                                      condition = "Test")
gt(head(Unstained_Signatures, 5))
Sample Experiment Condition Cluster Count UV1 UV2 UV3 UV4 UV5 UV6 UV7 UV8 UV9 UV10 UV11 UV12 UV13 UV14 UV15 UV16 V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 YG1 YG2 YG3 YG4 YG5 YG6 YG7 YG8 YG9 YG10 R1 R2 R3 R4 R5 R6 R7 R8
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_04-B3_05 746 0.1 0.2 0.1 0.2 0.2 0.3 0.6 0.4 0.4 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.3 0.5 0.5 0.8 0.8 1 0.8 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.1 0.3 0.4 0.5 0.4 0.3 0.3 0.2 0.2 0.2 0.1 0.1 0.1 0.1 0.1 0.2 0.1 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_05-B3_05 733 0.1 0.2 0.2 0.2 0.2 0.4 0.6 0.5 0.4 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.3 0.5 0.5 0.8 0.8 1 0.8 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.1 0.3 0.4 0.5 0.4 0.3 0.3 0.2 0.2 0.2 0.1 0.1 0.1 0.1 0.1 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_04-B3_04 373 0.1 0.2 0.1 0.2 0.2 0.3 0.6 0.4 0.4 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.3 0.5 0.5 0.8 0.7 1 0.8 0.5 0.6 0.3 0.2 0.2 0.2 0.2 0.1 0.3 0.3 0.4 0.3 0.3 0.2 0.2 0.2 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_05-B3_04 335 0.1 0.2 0.2 0.2 0.3 0.4 0.6 0.5 0.4 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.3 0.5 0.5 0.8 0.8 1 0.8 0.5 0.6 0.3 0.2 0.2 0.2 0.2 0.1 0.3 0.3 0.4 0.3 0.3 0.2 0.2 0.2 0.2 0.1 0.1 0.1 0.1 0.1 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_05-B3_06 318 0.1 0.2 0.2 0.2 0.2 0.4 0.6 0.5 0.4 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.3 0.5 0.5 0.8 0.8 1 0.8 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.1 0.3 0.4 0.6 0.4 0.3 0.3 0.2 0.2 0.2 0.1 0.1 0.1 0.1 0.1 0.2 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1

As you can see, we have a Cluster column following the peak naming format mentioned above, as well as the count of the number of cells that shared this signature. We also get back within the data.frame the normalized signature values for respective detectors that can be used to plot the signatures.

TheData <- Unstained_Signatures %>% select(-any_of(c("Sample", "Experiment", "Condition", "Count"))) %>% rename(Sample=Cluster)

TheFluors <- TheData %>% pull(Sample)
TheFluors <- TheFluors[1:10]

Luciernaga_Subpeaks <- QC_ViewSignature(x=TheFluors, data=TheData, Normalize = TRUE)
plotly::ggplotly(Luciernaga_Subpeaks)

As can be seen, this approach improves on the ability to capture the variation present within the UV8 detector.

Two important arguments to mention are Increments and SecondaryPeaks. Increments are by default set to 0.1, so 10% increments. So if a secondary peak is 0.18 of the primary peak value, it would get rounded to the nearest 0.1 increment interval (0.2). In Luciernaga’s current implementation, 0.1 is the lowest value verified to work, but it can be increased more broadly to 0.2.

Unstained_Signatures <- Luciernaga_QC(x=MyUnstainedGatingSet[2],
                                      subsets="lymphocytes",
                                      removestrings=removestrings,
                                      sample.name="GUID",
                                      unmixingcontroltype = "cells",
                                      Unstained = TRUE,
                                      ratiopopcutoff = 0.001,
                                      Verbose = FALSE,
                                      AFOverlap = AFOverlap, 
                                      stats = "median",
                                      ExportType = "data",
                                      SignatureReturnNow = FALSE,
                                      outpath = NULL,
                                      Increments=0.2,
                                      experiment = "UnstainedSignature",
                                      condition = "Test")
gt(head(Unstained_Signatures, 5))
Sample Experiment Condition Cluster Count UV1 UV2 UV3 UV4 UV5 UV6 UV7 UV8 UV9 UV10 UV11 UV12 UV13 UV14 UV15 UV16 V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 YG1 YG2 YG3 YG4 YG5 YG6 YG7 YG8 YG9 YG10 R1 R2 R3 R4 R5 R6 R7 R8
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_04-B3_06 1410 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.8 0.8 1 0.8 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.4 0.4 0.6 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_06-B3_06 1307 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.8 0.8 1 0.8 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.4 0.4 0.6 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_04-B3_04 549 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.8 0.8 1 0.8 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.4 0.4 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_06-B3_04 433 0.2 0.2 0.2 0.2 0.4 0.4 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.8 0.8 1 0.8 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.4 0.4 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_06-B3_08 93 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.6 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.8 0.8 1 1.0 0.6 0.8 0.4 0.2 0.2 0.2 0.2 0.2 0.4 0.4 0.8 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2
TheData <- Unstained_Signatures %>% select(-any_of(c("Sample", "Experiment", "Condition", "Count"))) %>% rename(Sample=Cluster)

TheFluors <- TheData %>% pull(Sample)
TheFluors <- TheFluors[1:10]

Luciernaga_Subpeaks <- QC_ViewSignature(x=TheFluors, data=TheData, Normalize = TRUE)
plotly::ggplotly(Luciernaga_Subpeaks)

Similarly, SecondaryPeaks default is set to 2, so allowing for a fluorescent with 3 separate peaks to be characterized. This works for most single-color unmixing controls, but can also be currently increased up to 8 peaks (which is used to characterize the QC bead and raw full-stained signatures, as described in vignettes elsewhere). Let’s decrease it to 1 for this example:

Unstained_Signatures <- Luciernaga_QC(x=MyUnstainedGatingSet[2],
                                      subsets="lymphocytes",
                                      removestrings=removestrings,
                                      sample.name="GUID",
                                      unmixingcontroltype = "cells",
                                      Unstained = TRUE,
                                      ratiopopcutoff = 0.001,
                                      Verbose = FALSE,
                                      AFOverlap = AFOverlap, 
                                      stats = "median",
                                      ExportType = "data",
                                      SignatureReturnNow = FALSE,
                                      outpath = NULL,
                                      Increments=0.2,
                                      SecondaryPeaks=1,
                                      experiment = "UnstainedSignature",
                                      condition = "Test")
gt(head(Unstained_Signatures, 5))
Sample Experiment Condition Cluster Count UV1 UV2 UV3 UV4 UV5 UV6 UV7 UV8 UV9 UV10 UV11 UV12 UV13 UV14 UV15 UV16 V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 YG1 YG2 YG3 YG4 YG5 YG6 YG7 YG8 YG9 YG10 R1 R2 R3 R4 R5 R6 R7 R8
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_04 2013 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.8 0.8 1 0.8 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.4 0.4 0.6 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_06 1836 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.8 0.8 1 0.8 0.6 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.4 0.4 0.6 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_08 41 0.2 0.2 0.2 0.2 0.4 0.4 0.8 0.8 0.6 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 1.0 0.8 1 0.8 0.6 0.8 0.4 0.2 0.2 0.2 0.2 0.2 0.4 0.4 0.6 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2
INF071_PMA_Unstained UnstainedSignature Test V7_10-UV8_02 30 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.2 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 0.8 0.8 1 1.0 0.6 0.8 0.4 0.4 0.4 0.2 0.2 0.2 0.4 0.4 0.6 0.4 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2
INF071_PMA_Unstained UnstainedSignature Test V8_10-V7_10 22 0.2 0.2 0.2 0.2 0.2 0.4 0.7 0.6 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.4 0.6 0.6 1.0 0.8 1 1.0 0.6 0.8 0.4 0.2 0.2 0.2 0.2 0.2 0.4 0.4 0.6 0.4 0.4 0.4 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2 0.2

One challenge is that for every additional peak, at 0.1 increments, the number of potential combinations a subcluster name can take increases, the end number of subclusters increase, and the fewer cells per subcluster result. At the same time, we get into the “Poisson” noise discussion territory where only people named David typically thread. At a certain point of increasing points we are evaluating at, every single autofluorescent cell eventually clusters only with itself at the typical number of cells collected for our experiments. So my advice for now is to set the Increments and SecondaryPeak arguments for what you need to achieve stable resolution/characterization/unmixing in your particular context.

Data Export Arguments

With all the above, we have covered all the important arguments related to Unstained cell unmixing controls. For the examples above, we specified returntype == “data”, which returned the summaries that could be used for the reporting functions in Luciernaga. Alternatively, we could export the purified signatures as .fcs files, by setting returntype = “fcs”, and providing a file path to our desired storage location via the outpath argument.

These files are compatible with other flow cytometry software, and can be used to assist in unmixing. This can be useful when a single-color is experiencing quality issues, or you want to create a tag specific to a certain subtype of autofluorescence. In the context of unstained unmixing controls, this would resemble the following code (sending to a temporary folder for this example):

#StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")
TemporaryStorage <- tempdir()


Unstained_Signatures <- Luciernaga_QC(x=MyUnstainedGatingSet[2],
                                      subsets="lymphocytes",
                                      removestrings=removestrings,
                                      sample.name="GUID",
                                      unmixingcontroltype = "cells",
                                      Unstained = TRUE,
                                      ratiopopcutoff = 0.001,
                                      Verbose = FALSE,
                                      AFOverlap = AFOverlap, 
                                      stats = "median",
                                      ExportType = "fcs",
                                      SignatureReturnNow = FALSE,
                                      outpath = TemporaryStorage,
                                      Increments=0.1,
                                      SecondaryPeaks=2,
                                      experiment = "UnstainedSignature",
                                      condition = "Test")

TheFCSFileOutputs <- list.files(TemporaryStorage, pattern="fcs")
TheFCSFileOutputs
#> [1] "INF071_PMA_Unstained_V710UV803B305.fcs"
#> [2] "INF071_PMA_Unstained_V710UV804B304.fcs"
#> [3] "INF071_PMA_Unstained_V710UV804B305.fcs"
#> [4] "INF071_PMA_Unstained_V710UV804B306.fcs"
#> [5] "INF071_PMA_Unstained_V710UV805B304.fcs"
#> [6] "INF071_PMA_Unstained_V710UV805B305.fcs"
#> [7] "INF071_PMA_Unstained_V710UV805B306.fcs"

To visualize what these may look like on their return:

TheFCSFileOutputs <- list.files(TemporaryStorage, pattern="fcs", full.names=TRUE)
TheIsolatedFCS <- load_cytoset_from_fcs(TheFCSFileOutputs[1], truncate_max_range = FALSE, transformation = FALSE)
TheIsolatedGS <- GatingSet(TheIsolatedFCS)

FCSView <- Utility_IterativeGating(x=TheIsolatedGS[1], subset="root", gate = NULL, xValue="UV8-A", yValue="B3-A",
                                   sample.name="GUID", removestrings=".fcs", bins=100)
FCSView
#> [[1]]

Additionally, providing argument Brightness = TRUE will return a .csv file with the report data. This output is used by Luciernaga_Tree() and a couple other of the report functions we will cover later.

#StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")
TemporaryStorage <- tempdir()

TheUnstaineds <- map(.x=MyUnstainedGatingSet[2], .f=Luciernaga_QC, 
                     subsets="lymphocytes", removestrings=removestrings,
                     sample.name="GUID", unmixingcontroltype = "cells",
                     Unstained = TRUE, ratiopopcutoff = 0.001,
                     Verbose = FALSE, AFOverlap = AFOverlap, 
                     stats = "median", ExportType = "fcs",
                     SignatureReturnNow = FALSE, outpath = TemporaryStorage,
                     Increments=0.1, SecondaryPeaks=2,
                     experiment = "UnstainedSignature",
                     condition = "Test", Brightness = TRUE)

BrightnessCSV <- list.files(TemporaryStorage, pattern="csv")
BrightnessCSV
#> [1] "RelativeBrightnessINF071_PMA_Unstained.csv"
BrightnessCSV <- list.files(TemporaryStorage, pattern="csv", full.names=TRUE)
TheBrightnessInformation <- read.csv(BrightnessCSV[1], check.names = FALSE)
head(TheBrightnessInformation, 5)
#>              Cluster Brightness Detector1 Detector1Value Detector2
#> 1 V7_10-UV8_04-B3_05         19        V7             10       UV8
#> 2 V7_10-UV8_05-B3_05         20        V7             10       UV8
#> 3 V7_10-UV8_04-B3_04         18        V7             10       UV8
#> 4 V7_10-UV8_05-B3_04         19        V7             10       UV8
#> 5 V7_10-UV8_05-B3_06         21        V7             10       UV8
#>   Detector2Value Detector3 Detector3Value Detector1Raw Detector2Raw
#> 1              4        B3              5     5572.359     1988.379
#> 2              5        B3              5     5049.207     2240.696
#> 3              4        B3              4     5381.337     1924.007
#> 4              5        B3              4     5008.231     2190.865
#> 5              5        B3              6     4722.747     2105.891
#>   Detector3Raw Count     Time      UV1      UV2      UV3      UV4       UV5
#> 1     2517.288   746 375075.5 425.2763 701.2447 526.6494 672.3500  926.2291
#> 2     2271.021   733 359993.0 450.5638 785.2513 614.5607 722.7763 1001.7569
#> 3     1968.330   373 370222.0 395.0056 728.1313 537.3594 682.3907  959.2888
#> 4     1820.461   335 380210.0 458.8194 791.7219 642.3026 779.1525 1005.4013
#> 5     2528.843   318 316452.0 414.3060 683.8781 547.5116 655.8016  911.5400
#>        UV6      UV7      UV8      UV9     UV10     UV11     UV12     UV13
#> 1 1487.798 2846.778 1988.379 1969.524 769.5953 511.3653 295.6035 237.9256
#> 2 1580.841 2923.979 2240.696 2021.736 748.2869 468.9344 281.6581 242.0906
#> 3 1536.439 2870.354 1924.007 1992.655 762.1950 468.7113 302.2600 269.3863
#> 4 1613.343 2879.280 2190.865 1928.693 710.6531 465.0669 256.7425 208.6219
#> 5 1450.015 2783.819 2105.891 1893.588 681.2379 422.1525 242.7228 174.8928
#>       UV14     UV15     UV16    SSC-W   SSC-H     SSC       V1       V2
#> 1 288.0172 227.9966 211.2250 683829.4 1107675 1261012 498.7125 1553.991
#> 2 278.0881 188.7638 175.4506 677780.6 1184736 1341172 470.5250 1465.956
#> 3 278.6831 202.4488 194.2675 679364.5 1091629 1250144 480.7000 1487.956
#> 4 289.9138 258.8250 169.9469 676423.9 1118851 1267046 433.8125 1438.731
#> 5 250.4206 198.6184 142.7628 674595.1 1153897 1301204 423.3969 1337.153
#>         V3       V4       V5       V6       V7       V8       V9      V10
#> 1 2389.991 2714.181 4178.934 4014.313 5572.359 4092.859 2830.163 3114.341
#> 2 2207.975 2498.512 3834.531 3721.919 5049.207 3793.763 2586.238 2864.744
#> 3 2245.031 2556.331 3892.694 3766.263 5381.337 3829.238 2562.519 2879.869
#> 4 2189.206 2489.575 3741.788 3533.888 5008.231 3657.500 2464.550 2757.219
#> 5 2084.809 2350.425 3551.556 3501.300 4722.747 3607.175 2461.009 2723.016
#>        V11      V12      V13      V14      V15      V16    FSC-W   FSC-H
#> 1 1682.966 982.3000 864.8063 757.2812 662.9906 366.1969 675626.4 1318871
#> 2 1516.006 844.7313 740.5750 643.2250 566.6375 332.7500 676409.3 1406765
#> 3 1546.394 878.4875 816.6813 714.4500 661.9938 361.4875 673448.2 1348645
#> 4 1437.081 818.7438 704.1375 657.7313 543.6063 313.2250 674743.6 1397406
#> 5 1475.994 805.3031 713.9344 644.1531 563.3031 323.7438 675404.4 1393753
#>       FSC  SSC-B-W SSC-B-H    SSC-B       B1       B2       B3       B4
#> 1 1491917 669505.2  729827 821310.0 1572.070 1809.549 2517.288 1703.298
#> 2 1586855 665302.0  809745 886955.3 1349.236 1600.684 2271.021 1543.906
#> 3 1517825 668050.1  732468 819600.6 1363.656 1610.147 1968.330 1530.709
#> 4 1568460 663978.6  764488 842286.2 1246.429 1475.861 1820.461 1445.154
#> 5 1568841 662644.3  780609 861347.5 1404.952 1595.148 2528.843 1517.093
#>         B5        B6       B7       B8       B9      B10      B11      B12
#> 1 1449.693 1177.2256 858.7625 644.5869 671.5278 444.7990 324.1925 312.4119
#> 2 1330.696 1018.9275 756.2775 544.6769 600.4256 359.0194 292.7775 255.3756
#> 3 1349.944 1083.3668 789.4950 612.4637 613.1719 416.6994 302.3694 294.1938
#> 4 1166.926  966.7194 671.5600 522.9825 543.5825 351.1012 232.0719 255.9550
#> 5 1272.887 1025.2040 720.0987 535.5356 558.9037 386.8937 298.1206 246.3631
#>        B13      B14      YG1      YG2      YG3      YG4      YG5      YG6
#> 1 214.4653 304.5259 635.7178 572.4478 593.8847 539.6681 440.1844 462.8353
#> 2 201.4294 215.9781 601.3425 471.3338 471.9581 454.8919 345.9731 353.3269
#> 3 206.5794 269.2806 596.9719 499.1531 491.9381 512.5425 386.9738 433.6631
#> 4 173.8769 234.1962 545.8425 440.9475 443.7919 422.6325 312.8119 306.2906
#> 5 173.7803 253.5087 559.3359 454.3716 447.9197 446.1506 352.1128 327.8316
#>        YG7      YG8      YG9     YG10       R1       R2       R3       R4
#> 1 522.7753 281.9747 241.6678 174.5475 218.5491 275.8259 300.2622 311.2797
#> 2 448.9950 185.5781 191.8913 181.0688 170.0650 256.5806 248.8825 300.1562
#> 3 531.4125 265.2206 214.7850 172.3275 183.4131 286.9494 257.8519 306.5831
#> 4 433.8019 166.9856 198.6206 137.9869 181.5769 256.0156 235.8875 252.1313
#> 5 435.0853 181.0341 223.8037 115.7522 174.5144 218.6197 244.5038 264.4906
#>         R5       R6        R7       R8
#> 1 167.3459 169.2528 188.53343 102.6888
#> 2 156.7169 117.0963 137.93062 108.9744
#> 3 159.6125 180.9413 126.91312 105.8669
#> 4 177.5513 154.5981 136.16499  49.1550
#> 5 166.7809 110.4928  81.39531 109.6453

Similarly, in combination with purrr map function, we can iterate over our GatingSet to generate subcluster .fcs files for all our Unstained samples in one go.

#StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")
TemporaryStorage <- tempdir()

TheUnstaineds <- map(.x=MyUnstainedGatingSet[1:3], .f=Luciernaga_QC, 
                     subsets="lymphocytes", removestrings=removestrings,
                     sample.name="GUID", unmixingcontroltype = "cells",
                     Unstained = TRUE, ratiopopcutoff = 0.001,
                     Verbose = FALSE, AFOverlap = AFOverlap, 
                     stats = "median", ExportType = "fcs",
                     SignatureReturnNow = FALSE, outpath = StorageLocation,
                     Increments=0.1, SecondaryPeaks=2,
                     experiment = "UnstainedSignature",
                     condition = "Test", Brightness = TRUE)

Single Color Unmixing Controls

Now that we have covered unstained unmixing controls, let’s look at how Luciernaga_QC() works with single-color unmixing controls. Let’s set the returnsignaturenow = TRUE as we had done previously when starting with the unstained samples.

StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")

pData(MyGatingSet[2])
#>                                        name
#> CCR6_BV786(Cells).fcs CCR6_BV786(Cells).fcs

SingleStain_Signatures <- Luciernaga_QC(x=MyGatingSet[2],
                                      subsets="lymphocytes",
                                      removestrings=removestrings,
                                      sample.name="GUID",
                                      unmixingcontroltype = "cells",
                                      Unstained = FALSE,
                                      ratiopopcutoff = 0.01,
                                      Verbose = TRUE,
                                      AFOverlap = AFOverlap, 
                                      stats = "median",
                                      ExportType = "data",
                                      SignatureReturnNow = TRUE,
                                      outpath = NULL)
#> 0.09 of all events were negative and will be rounded to 0
#>   Fluors Counts
#> 1     V7   3671
#> 2    V15   1356
#> 3    UV7    645
#> 4     V5     61
#> [1] "V15"
#> Normalizing Data for Signature Comparison
#>   Fluors Counts
#> 1     V7   3671
#> 2    V15   1356
#> 3    UV7    645
#> 4     V5     61

V7 <- Luciernaga_QC(x=MyGatingSet[2], subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                    ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data", SignatureReturnNow = TRUE, outpath = NULL,
                    desiredAF="V7-A")
#> Normalizing Data for Signature Comparison

V15 <- Luciernaga_QC(x=MyGatingSet[2], subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                    ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data", SignatureReturnNow = TRUE, outpath = NULL,
                    desiredAF="V15-A")
#> Normalizing Data for Signature Comparison

UV7 <- Luciernaga_QC(x=MyGatingSet[2], subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                    ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data", SignatureReturnNow = TRUE, outpath = NULL,
                    desiredAF="UV7-A")
#> Normalizing Data for Signature Comparison

V5 <- Luciernaga_QC(x=MyGatingSet[2], subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                    ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data", SignatureReturnNow = TRUE, outpath = NULL,
                    desiredAF="V5-A")
#> Normalizing Data for Signature Comparison

data <- rbind(V7, V15, UV7, V5)
Sample <- data.frame(Sample=c("V7", "V15", "UV7", "V5"))
Data <- cbind(Sample, data)

Fluors <- Data %>% pull(Sample)
Raw_Results <- QC_ViewSignature(x=Fluors, data=Data, Normalize = FALSE)
plotly::ggplotly(Raw_Results)
Normalized_Results <- QC_ViewSignature(x=Fluors, data=Data) 
#> Normalizing Data for Signature Comparison
plotly::ggplotly(Normalized_Results)

As we can see from the returned peak detectors, for this BV786 single-stained unmixing control, there were 4 detectors which cells from the sample had primary peaks on. Looking at the signatures (and referencing to the results from our Unstained samples looked at previously) three are unstained peak detectors (V7, UV7, V5). Consequently, the remaining detector (V15) is likely to be of antibody-staining origin. This is particularly obvious when we look at the normalized signature. In the case of Raw MFI plot, the antibody-stained cells are way brighter than the underlying autofluorescence of cells that didn’t get stained by the antibody (due to not having the antigen).

Please note that at this stage, autofluorescence background has not yet been subtracted from the single-color unmixing control, which is why we can see for ID_V15 residual autofluorescence present in both the raw and normalized signature. In the normalized signature, it materializes as minor peaks below 0.2 at both the UV7-A and V7-A. It’s this bit of background that we need to properly extract in order to provide a good signature for unmixing.

Luciernaga_QC() carries this process in a similar manner for all single-color unmixing controls, enumerating peak detectors, removing those listed by the AFOverlap .csv for unstained from consideration, and then focusing on the likely antibody-stain origin detectors for further processing.

The advantage of isolating peak detectors in this manner is that it allows Luciernaga to profile unusual fluorescence signals that may be present within a single-color unmixing control. These could be rarer autofluorescences, but they could also be indicators for the overall quality of the antibody-fluorophore conjugation.

It’s because of this that we can also see why the AFOverlap is needed when a fluorophore shares it’s peak detector with the main autofluorescence peaks. Let’s take a look at what happens with BV510 for this specific case:

StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")

#pData(MyGatingSet)

SingleStain_Signatures <- Luciernaga_QC(x=MyGatingSet[16],
                                      subsets="lymphocytes",
                                      removestrings=removestrings,
                                      sample.name="GUID",
                                      unmixingcontroltype = "cells",
                                      Unstained = FALSE,
                                      ratiopopcutoff = 0.01,
                                      Verbose = TRUE,
                                      AFOverlap = AFOverlap, 
                                      stats = "median",
                                      ExportType = "data",
                                      SignatureReturnNow = TRUE,
                                      outpath = NULL)
#> 0.08 of all events were negative and will be rounded to 0
#>   Fluors Counts
#> 1     V7   2122
#> [1] "V7"
#> Only a single detector present. If this was not an autofluorescence overlap
#>               fluourophore, it would suggest there was no antibody staining, or everything
#>               was overstained. Please investigate further.
#> Error in Luciernaga_QC(x = MyGatingSet[16], subsets = "lymphocytes", removestrings = removestrings, : Only one detector present and no external cell autofluorescence signature was provided
#>            for subtraction. Please provision the CellAF argument.

In this case, the function errors out and calls for an external autofluorescence signature to be provided to the ExternalAF argument. When we look at the Verbose = TRUE message, we can see that for this particular sample, only V7 is present. We all address how to deal this shortly.

While just working off the peak detector works quite nicely for bright fluorophores, this is not always the case. Brightness is impacted not only by the fluorophore itself, but also by the antigen density on the cell surfact that the fluorophore-conjugated antibodies latch on to.

A good highlighting example of this more-complicated side is our PerCP-Cy5.5 CD26 unmixing control. In this panel, it was added after fixation, and had some issues with degradatation and non-specific/background staining:

StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")

#pData(MyGatingSet)

SingleStain_Signatures <- Luciernaga_QC(x=MyGatingSet[9],
                                      subsets="lymphocytes",
                                      removestrings=removestrings,
                                      sample.name="GUID",
                                      unmixingcontroltype = "cells",
                                      Unstained = FALSE,
                                      ratiopopcutoff = 0.01,
                                      Verbose = TRUE,
                                      AFOverlap = AFOverlap, 
                                      stats = "median",
                                      ExportType = "data",
                                      SignatureReturnNow = TRUE,
                                      outpath = NULL)
#> 0.02 of all events were negative and will be rounded to 0
#>   Fluors Counts
#> 1     V7   2675
#> 2     B9   2383
#> 3    UV7    307
#> 4    B14    285
#> 5     R3     83
#> [1] "B9"  "B14" "R3"
#> Normalizing Data for Signature Comparison
#>   Fluors Counts
#> 1     V7   2675
#> 2     B9   2383
#> 3    UV7    307
#> 4    B14    285
#> 5     R3     83

V7 <- Luciernaga_QC(x=MyGatingSet[9], subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                    ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data", SignatureReturnNow = TRUE, outpath = NULL,
                    desiredAF="V7-A")
#> Normalizing Data for Signature Comparison

B9 <- Luciernaga_QC(x=MyGatingSet[9], subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                    ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data", SignatureReturnNow = TRUE, outpath = NULL,
                    desiredAF="B9-A")
#> Normalizing Data for Signature Comparison

UV7 <- Luciernaga_QC(x=MyGatingSet[9], subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                    ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data", SignatureReturnNow = TRUE, outpath = NULL,
                    desiredAF="UV7-A")
#> Normalizing Data for Signature Comparison

B14 <- Luciernaga_QC(x=MyGatingSet[9], subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                    ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data", SignatureReturnNow = TRUE, outpath = NULL,
                    desiredAF="B14-A")
#> Normalizing Data for Signature Comparison

R3 <- Luciernaga_QC(x=MyGatingSet[9], subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                    ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data", SignatureReturnNow = TRUE, outpath = NULL,
                    desiredAF="R3-A")
#> Normalizing Data for Signature Comparison

data <- rbind(V7, B9, UV7, B14, R3)
Sample <- data.frame(Sample=c("V7", "B9", "UV7", "B14", "R3"))
Data <- cbind(Sample, data)

Fluors <- Data %>% pull(Sample)
Results <- QC_ViewSignature(x=Fluors, data=Data, Normalize = FALSE) 
plotly::ggplotly(Results)
Results <- QC_ViewSignature(x=Fluors, data=Data) 
#> Normalizing Data for Signature Comparison
plotly::ggplotly(Results)

As we can see from the signatures, there are only a few detectors that reach peak detector status. However, unlike was the case with BV786, the peak-detectors corresponding to antibody-staining are not that much brighter than the underlying autofluorescence in terms on raw MFI. Consequently, without subtracting out the background signal, the normalized signatures end up introducing autofluorescence artefacts to what should be fluorophore specific peaks, and fluorophore contributions to what should be purely autofluorescence detectors. Consequently, an additional step to process single-stained cells is needed.

One way that we can do this is through the provisioning of an external autofluorescence signature. While this is required for the autofluorescence overlap fluorophores, it is an option that can be implemented for all fluorophores if desired.

# pData(MyUnstainedGatingSet[1])

V7_Signature <- Luciernaga_QC(x=MyUnstainedGatingSet[1], desiredAF = "V7-A",
                    subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells",
                    Unstained = TRUE, ratiopopcutoff = 0.001, Verbose = FALSE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> Normalizing Data for Signature Comparison

PerCPCy55_Signatures <- Luciernaga_QC(x=MyGatingSet[9], subsets="lymphocytes", removestrings=removestrings,
                        sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                        ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                        ExportType = "data", SignatureReturnNow = FALSE, outpath = NULL,
                        experiment = "PerCP", condition = "Test", Increments=0.1, Subtraction = "External", 
                        CellAF=V7_Signature)

Data <- PerCPCy55_Signatures %>% filter(Count > 60) %>% select(-Sample, -Experiment, -Condition, -Count) %>%
  rename(Sample = Cluster)

Fluorophores <- Data %>% pull(Sample)

AfterSubtraction <- QC_ViewSignature(x=Fluorophores, data=Data)

plotly::ggplotly(AfterSubtraction)

As we can see, we are left with signatures more closely resembling that of PerCP-Cy5.5.

PerCPCy5.5_Sample <- Data[1,]

x <- PerCPCy5.5_Sample %>% pull(Sample)

ThePerCPCy5.5 <- QC_WhatsThis(x=x, data=PerCPCy5.5_Sample, NumberHits = 5, returnPlots = TRUE)
ThePerCPCy5.5[1]
#> [[1]]
#>       Fluorophore ID_B9_10-V13_07-YG6_05
#> 1     cFluor B690                   0.94
#> 2     PerCP-Cy5.5                   0.94
#> 3   PerCP-Vio 700                   0.93
#> 4 StarBright B700                   0.92
#> 5           BB700                   0.89
plotly::ggplotly(ThePerCPCy5.5[[2]])

As we can see, this did work well and improved the returned signature. An alternative way, is to rely on an internal autofluorescence. As was the case in BV786, for many single-color unmixing controls, there are quite a few cells that remain unstained within the sample that could potentially serve as an internal autofluorescence negative. This is particularly helpful in cases where a mismatch ends up occurring vs an external unstained sample during the processing and we end up with mismatching autofluorescence. However, if the antibody has a lot of non-specific background, it can contaminate the unstained peak detectors as we saw in the case above with PerCP-Cy5.5.

Our way of handling this issue when using Luciernaga_QC() internal negatives for subtraction is as follows. We set Subtraction = “Internal”, and leave desiredAF = NULL (the R equivalent to empty). The main autofluorescent overlap peak is worked out internally via the AFOverlap csv and the count of peak detectors. After the initial peaks are characterized, the cells in a sample are initially grouped on basis of shared peak detectors. Once this is done, for each “likely” antibody detector, they are subclustered on the basis of the normalized values of both that detector and the Main Autofluorescent detector.

What we end up with, for the case above, would be a continuum of clusters ranging from those where the antibody stain is way brighter than underlying autofluorescence (B9_10-V7_00) to cells with equal brightness for both antibody and autofluorescence (B9_10-V7_10), all the way through cells that only have autofluorescence (V7_10-B9_00). From this continuum, the bin with the least amount of “antibody” detector is selected as the “uncontaminated” autofluorescence, and these cells are filtered out, and the average autofluorescence signature is extracted from them. This in turn is subtracted from all initial subclustering cells removing the background. At this point, cells with the subtracted values are re-enumerated on basis of their peak detector, and passed onward in the function to the rolling local maxima step that was described in the unstained section of the vignette to figure out the secondary peak detectors needed to continue with the pipeline.

Does this more complicated way work, and is it worth the hassle? Let’s compare the external AF from the above to the internal AF method below:

InternalPerCPCy55_Signatures <- Luciernaga_QC(x=MyGatingSet[9], subsets="lymphocytes", removestrings=removestrings,
                        sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                        ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                        ExportType = "data", SignatureReturnNow = FALSE, outpath = NULL,
                        experiment = "PerCP", condition = "Test", Increments=0.1, Subtraction = "Internal")

Data <- InternalPerCPCy55_Signatures %>% filter(Count > 60) %>% select(-Sample, -Experiment, -Condition, -Count) %>%
  rename(Sample = Cluster)

Fluorophores <- Data %>% pull(Sample)

AfterSubtraction <- QC_ViewSignature(x=Fluorophores, data=Data)

plotly::ggplotly(AfterSubtraction)
InternalPerCPCy5.5_Sample <- Data[1,]

x <- InternalPerCPCy5.5_Sample %>% pull(Sample)

TheInternalPerCPCy5.5 <- QC_WhatsThis(x=x, data=InternalPerCPCy5.5_Sample, NumberHits = 5, returnPlots = TRUE)
TheInternalPerCPCy5.5[1]
#> [[1]]
#>       Fluorophore ID_B9_10-YG6_06-V13_06
#> 1     cFluor B690                   0.98
#> 2     PerCP-Cy5.5                   0.98
#> 3 StarBright B700                   0.93
#> 4   PerCP-Vio 700                   0.92
#> 5           BB700                   0.89
plotly::ggplotly(TheInternalPerCPCy5.5[[2]])

As you can see, that the main peaks are similar for both methods (which is good considering they are the same .fcs file), but better matching signature of the secondary peaks. In our testing, either approach provides good results, with internal AF providing some additional usefulness in cases where the experiment required troubleshooting. Being aware of possible scenarios in which picking the least contaminated bin could cause issues will be discussed in the unmixing vignette along with the broader discussion on brightness role in the process.

And finally for good measure, let’s generate the signature for BV510 using the externalAF to demonstrate the AFOverlap cases as well. For these, we provision the CellAF argument:

pData(MyGatingSet[16])
#>                                            name
#> CD45RA_BV510(Cells).fcs CD45RA_BV510(Cells).fcs

V7_Signature <- Luciernaga_QC(x=MyUnstainedGatingSet[1], desiredAF = "V7-A",
                    subsets="lymphocytes", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "cells",
                    Unstained = TRUE, ratiopopcutoff = 0.001, Verbose = FALSE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> Normalizing Data for Signature Comparison

BV510 <- Luciernaga_QC(x=MyGatingSet[16], subsets="lymphocytes", removestrings=removestrings,
                        sample.name="GUID", unmixingcontroltype = "cells", Unstained = FALSE,
                        ratiopopcutoff = 0.001, Verbose = FALSE, AFOverlap = AFOverlap, stats = "median",
                        ExportType = "data", SignatureReturnNow = FALSE, outpath = NULL,
                        experiment = "PerCP", condition = "Test", Increments=0.1, Subtraction="Internal",
                        CellAF=V7_Signature)
#> Only a single detector present. If this was not an autofluorescence overlap
#>               fluourophore, it would suggest there was no antibody staining, or everything
#>               was overstained. Please investigate further.

Data <- BV510 %>% filter(Count > 60) %>% select(-Sample, -Experiment, -Condition, -Count) %>%
  rename(Sample = Cluster)

Fluorophores <- Data %>% pull(Sample)

AfterSubtraction <- QC_WhatsThis(x=Fluorophores, data=Data, NumberHits=5, returnPlots = TRUE)

AfterSubtraction[1]
#> [[1]]
#>      Fluorophore ID_V7_10-V6_08-UV8_04
#> 1          BV510                  0.97
#> 2 LIVE DEAD Aqua                  0.96
#> 3    Zombie Aqua                  0.94
#> 4       VioGreen                  0.93
#> 5   Krome Orange                  0.91
plotly::ggplotly(AfterSubtraction[[2]])

Small rounding errors contribute to the the plateau appearance at 0.1 compared to the reference control and remain on my fix-list.

Data Export

For the above examples, we had ExportType set to “data”. As was the case with the Unstained unmixing controls, we can also export these as “.fcs” files for storage and use in other flow cytometry software. Unlike Unstained controls where nothing was subtracted, single color unmixing controls had values subtracted to derive the signatures that were used for the clustering, so they require a few additional arguments.

SCData can be set to either “subtracted” or “raw”. It dictates whether the values within the new “.fcs” file are the ones after background subtraction, or the original raw MFI values that would need to be subtracted from again. Please note, at this time, “raw” only works when Subtracted = “Internal_General” or “External”. We are still in process of designing a way for “Internal” to return raw values rather than the after subtraction ones.

What choice we select for SCData partly depends on what you plan to do with the .fcs files afterwards. If we are bringing in to other software (let’s use Cytek’s SpectroFlo for our current example) we would want to avoid having the software subtract out background autofluorescence from a Luciernaga .fcs output that has already had the background subtracted and was returned with SCData = “subtracted”. We would need to either export with SCData set to “raw”, or provision the file with a workaround to get around the subtraction step.

One way we have done this during development was to provide the NegativeType argument, which comes with the arguments “default”, “artificial” and “samples”. Default doesn’t add anything to the .fcs file. Artificial adds a certain number of events with MFI values for all detectors set to 0. Samples returns a certain number of events with the same MFI values corresponding to the autofluorescence signature used to subtract out background (currently restricted to Subtraction = “Internal_Generalized” and “External”).

If we are using a single-color unmixed .fcs file from Luciernaga within SpectroFlo, we would set the single color controls we are adding to use internal background subtraction within the software, and if 1) SCData was set to “subtracted” use NegativeType “artifical” to provision it with an internal negative consisting entirely of 0 MFI values which we can set the internal negative gate on. Consequently, no MFI values are further altered, and the single color unmixing control retains it’s signature for unmixing.

StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")
SingleStain_Signatures <- Luciernaga_QC(x=MyGatingSet[2], subsets="lymphocytes",
                                        removestrings=removestrings, sample.name="GUID",
                                        unmixingcontroltype = "cells", Unstained = FALSE,
                                        ratiopopcutoff = 0.001, Verbose = FALSE,
                                        AFOverlap = AFOverlap, stats = "median",
                                        ExportType = "fcs", SignatureReturnNow = FALSE,
                                        outpath = StorageLocation, Increments=0.1,
                                        SecondaryPeaks=2, experiment = "UnstainedSignature",
                                        condition = "Test", SCData="subtracted",NegativeType="artificial")

Alternatively, 2) if SCData was set to return “raw” MFI values (original values), we could provision NegativeType with “samples” to provide the background AF signature as events we could set the internal negative gate on to extract the same final signature within the software.

StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")
SingleStain_Signatures <- Luciernaga_QC(x=MyGatingSet[2], subsets="lymphocytes",
                                        removestrings=removestrings, sample.name="GUID",
                                        unmixingcontroltype = "cells", Unstained = FALSE,
                                        ratiopopcutoff = 0.001, Verbose = FALSE,
                                        AFOverlap = AFOverlap, stats = "median",
                                        ExportType = "fcs", SignatureReturnNow = FALSE,
                                        outpath = StorageLocation, Increments=0.1,
                                        SecondaryPeaks=2, experiment = "UnstainedSignature",
                                        condition = "Test", SCData="subtracted",NegativeType="samples")

And finally, if we are not bringing the files into a software that will try to extract additional background, we can leave SCData = “subtractd” and NegativeType = “default” to retain the data in the format it is returned by Luciernaga_QC()

TemporaryFolder <- tempdir()

SingleStain_Signatures <- Luciernaga_QC(x=MyGatingSet[2], subsets="lymphocytes",
                                        removestrings=removestrings, sample.name="GUID",
                                        unmixingcontroltype = "cells", Unstained = FALSE,
                                        ratiopopcutoff = 0.001, Verbose = FALSE,
                                        AFOverlap = AFOverlap, stats = "median",
                                        ExportType = "fcs", SignatureReturnNow = FALSE,
                                        outpath = TemporaryFolder, Increments=0.1,
                                        SecondaryPeaks=2, experiment = "UnstainedSignature",
                                        condition = "Test", SCData="subtracted", NegativeType="default")

ExportedFCSFiles <- list.files(TemporaryFolder, pattern="fcs")
ExportedFCSFiles

As you can tell, there are some coding decisions/bugs/consideration for this portion of the workflow that remain to be automated when I have time and the paper I need to graduate is submitted and I can justify yet again optimizing code with multiple moving parts. So until then, hang on, and reach out if with any questions.

And as always, in combination with the purrr map function, we can iterate over the entire GatingSet, returning isolated signatures for all single-color unmixing controls.

StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")
SingleStain_Signatures <- map(.x=MyGatingSet, .f=Luciernaga_QC, subsets="lymphocytes",
                              removestrings=removestrings, sample.name="GUID",
                              unmixingcontroltype = "cells", Unstained = FALSE,
                              ratiopopcutoff = 0.001, Verbose = FALSE,
                              AFOverlap = AFOverlap, stats = "median",
                              ExportType = "fcs", SignatureReturnNow = FALSE,
                              outpath = TemporaryFolder, Increments=0.1,
                              SecondaryPeaks=2, experiment = "UnstainedSignature",
                              condition = "Test", SCData="subtracted",
                              NegativeType="default"))

Bead Single-Color Signatures

We have covered most of the basic functionality to Luciernaga_QC() in the above examples, but we will briefly cover the adjustments when running unmixing controls derived with beads. While recovering the main fluorescence peak from stained beads is rather simple due to it’s substantially brighter MFI, in our experience with Invitrogen’s UltraComp eBeads Plus(TM) it was difficult to differentiate the unstained bead peaks from degrading antibody peaks. This is because unlike unstained cells, there is no one single bead autofluorescence peak, but rather due to the low levels of MFI background, what detector ends up being characterized as a peak detector resembles noisy distribution across all detectors.

pData(MyBeadsGatingSet)
#>                                                              name
#> CCR4_BUV615(Beads).fcs                     CCR4_BUV615(Beads).fcs
#> CCR6_BV786(Beads).fcs                       CCR6_BV786(Beads).fcs
#> CCR7_BV650(Beads).fcs                       CCR7_BV650(Beads).fcs
#> CD107a_APC-R700(Beads).fcs             CD107a_APC-R700(Beads).fcs
#> CD127_BV421(Beads).fcs                     CD127_BV421(Beads).fcs
#> CD16_APC(Beads).fcs                           CD16_APC(Beads).fcs
#> CD161_BV480(Beads).fcs                     CD161_BV480(Beads).fcs
#> CD25_PE-Cy5(Beads).fcs                     CD25_PE-Cy5(Beads).fcs
#> CD26_PerCP-Cy5.5(Beads).fcs           CD26_PerCP-Cy5.5(Beads).fcs
#> CD27_APC-Fire750(Beads).fcs           CD27_APC-Fire750(Beads).fcs
#> CD3_AlexaFluor488(Beads).fcs         CD3_AlexaFluor488(Beads).fcs
#> CD3_AlexaFluor647(Beads).fcs         CD3_AlexaFluor647(Beads).fcs
#> CD3_SparkBlue550(Beads).fcs           CD3_SparkBlue550(Beads).fcs
#> CD38_APC-Fire810(Beads).fcs           CD38_APC-Fire810(Beads).fcs
#> CD4_BUV805(Beads).fcs                       CD4_BUV805(Beads).fcs
#> CD45RA_BV510(Beads).fcs                   CD45RA_BV510(Beads).fcs
#> CD56_BV605(Beads).fcs                       CD56_BV605(Beads).fcs
#> CD62L_BUV395(Beads).fcs                   CD62L_BUV395(Beads).fcs
#> CD69_BUV563(Beads).fcs                     CD69_BUV563(Beads).fcs
#> CD7_BV711(Beads).fcs                         CD7_BV711(Beads).fcs
#> CD8_BUV496(Beads).fcs                       CD8_BUV496(Beads).fcs
#> CXCR3_BUV737(Beads).fcs                   CXCR3_BUV737(Beads).fcs
#> Dump_CD14_PacificBlue(Beads).fcs Dump_CD14_PacificBlue(Beads).fcs
#> Dump_CD19_PacificBlue(Beads).fcs Dump_CD19_PacificBlue(Beads).fcs
#> IFNg_BV750(Beads).fcs                       IFNg_BV750(Beads).fcs
#> NKG2D_PE(Beads).fcs                           NKG2D_PE(Beads).fcs
#> PD1_PE-Vio770(Beads).fcs                 PD1_PE-Vio770(Beads).fcs
#> TNFa_PE-Dazzle594(Beads).fcs         TNFa_PE-Dazzle594(Beads).fcs
#> Va7.2_AlexaFluor647(Beads).fcs     Va7.2_AlexaFluor647(Beads).fcs
#> VD2_BUV661(Beads).fcs                       VD2_BUV661(Beads).fcs

BV786 <- Luciernaga_QC(x=MyBeadsGatingSet[2], desiredAF = NULL,
                    subsets="singlets", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "beads",
                    Unstained = FALSE, ratiopopcutoff = 0.001, Verbose = TRUE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> 0.19 of all events were negative and will be rounded to 0
#> Returning Peak Bead Detector Medians
#>   TheDetector   TheMedian
#> 1         V15 394556.2500
#> 2         YG1    768.0050
#> 3          V7    713.0750
#> 4         UV9    764.5007
#> 5          V5    765.1188
#> 6          V3    735.7625
#> 7          V4    719.6750
#>   Fluors Counts
#> 1    V15   1146
#> [1] "V15"
#> Normalizing Data for Signature Comparison
#>   Fluors Counts
#> 1    V15   1146

For the staining beads we use, the only bright signal is the fluorophore stained beads as seen above. Beyond that, the next occupied peak detector is dictated often by noise and the normalize signature reflects this uncertainty, as we see when we set the desiredAF to retrieve the “YG1-A” instead.

pData(MyBeadsGatingSet)
#>                                                              name
#> CCR4_BUV615(Beads).fcs                     CCR4_BUV615(Beads).fcs
#> CCR6_BV786(Beads).fcs                       CCR6_BV786(Beads).fcs
#> CCR7_BV650(Beads).fcs                       CCR7_BV650(Beads).fcs
#> CD107a_APC-R700(Beads).fcs             CD107a_APC-R700(Beads).fcs
#> CD127_BV421(Beads).fcs                     CD127_BV421(Beads).fcs
#> CD16_APC(Beads).fcs                           CD16_APC(Beads).fcs
#> CD161_BV480(Beads).fcs                     CD161_BV480(Beads).fcs
#> CD25_PE-Cy5(Beads).fcs                     CD25_PE-Cy5(Beads).fcs
#> CD26_PerCP-Cy5.5(Beads).fcs           CD26_PerCP-Cy5.5(Beads).fcs
#> CD27_APC-Fire750(Beads).fcs           CD27_APC-Fire750(Beads).fcs
#> CD3_AlexaFluor488(Beads).fcs         CD3_AlexaFluor488(Beads).fcs
#> CD3_AlexaFluor647(Beads).fcs         CD3_AlexaFluor647(Beads).fcs
#> CD3_SparkBlue550(Beads).fcs           CD3_SparkBlue550(Beads).fcs
#> CD38_APC-Fire810(Beads).fcs           CD38_APC-Fire810(Beads).fcs
#> CD4_BUV805(Beads).fcs                       CD4_BUV805(Beads).fcs
#> CD45RA_BV510(Beads).fcs                   CD45RA_BV510(Beads).fcs
#> CD56_BV605(Beads).fcs                       CD56_BV605(Beads).fcs
#> CD62L_BUV395(Beads).fcs                   CD62L_BUV395(Beads).fcs
#> CD69_BUV563(Beads).fcs                     CD69_BUV563(Beads).fcs
#> CD7_BV711(Beads).fcs                         CD7_BV711(Beads).fcs
#> CD8_BUV496(Beads).fcs                       CD8_BUV496(Beads).fcs
#> CXCR3_BUV737(Beads).fcs                   CXCR3_BUV737(Beads).fcs
#> Dump_CD14_PacificBlue(Beads).fcs Dump_CD14_PacificBlue(Beads).fcs
#> Dump_CD19_PacificBlue(Beads).fcs Dump_CD19_PacificBlue(Beads).fcs
#> IFNg_BV750(Beads).fcs                       IFNg_BV750(Beads).fcs
#> NKG2D_PE(Beads).fcs                           NKG2D_PE(Beads).fcs
#> PD1_PE-Vio770(Beads).fcs                 PD1_PE-Vio770(Beads).fcs
#> TNFa_PE-Dazzle594(Beads).fcs         TNFa_PE-Dazzle594(Beads).fcs
#> Va7.2_AlexaFluor647(Beads).fcs     Va7.2_AlexaFluor647(Beads).fcs
#> VD2_BUV661(Beads).fcs                       VD2_BUV661(Beads).fcs

NotBV786 <- Luciernaga_QC(x=MyBeadsGatingSet[2], desiredAF = "YG1-A",
                    subsets="singlets", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "beads",
                    Unstained = FALSE, ratiopopcutoff = 0.001, Verbose = TRUE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> 0.19 of all events were negative and will be rounded to 0
#> Returning Peak Bead Detector Medians
#>   TheDetector   TheMedian
#> 1         V15 394556.2500
#> 2         YG1    768.0050
#> 3          V7    713.0750
#> 4         UV9    764.5007
#> 5          V5    765.1188
#> 6          V3    735.7625
#> 7          V4    719.6750
#>   Fluors Counts
#> 1    V15   1146
#> [1] "V15"
#> Normalizing Data for Signature Comparison
#>   Fluors Counts
#> 1    V15   1146

pData(MyBeadsGatingSet)
#>                                                              name
#> CCR4_BUV615(Beads).fcs                     CCR4_BUV615(Beads).fcs
#> CCR6_BV786(Beads).fcs                       CCR6_BV786(Beads).fcs
#> CCR7_BV650(Beads).fcs                       CCR7_BV650(Beads).fcs
#> CD107a_APC-R700(Beads).fcs             CD107a_APC-R700(Beads).fcs
#> CD127_BV421(Beads).fcs                     CD127_BV421(Beads).fcs
#> CD16_APC(Beads).fcs                           CD16_APC(Beads).fcs
#> CD161_BV480(Beads).fcs                     CD161_BV480(Beads).fcs
#> CD25_PE-Cy5(Beads).fcs                     CD25_PE-Cy5(Beads).fcs
#> CD26_PerCP-Cy5.5(Beads).fcs           CD26_PerCP-Cy5.5(Beads).fcs
#> CD27_APC-Fire750(Beads).fcs           CD27_APC-Fire750(Beads).fcs
#> CD3_AlexaFluor488(Beads).fcs         CD3_AlexaFluor488(Beads).fcs
#> CD3_AlexaFluor647(Beads).fcs         CD3_AlexaFluor647(Beads).fcs
#> CD3_SparkBlue550(Beads).fcs           CD3_SparkBlue550(Beads).fcs
#> CD38_APC-Fire810(Beads).fcs           CD38_APC-Fire810(Beads).fcs
#> CD4_BUV805(Beads).fcs                       CD4_BUV805(Beads).fcs
#> CD45RA_BV510(Beads).fcs                   CD45RA_BV510(Beads).fcs
#> CD56_BV605(Beads).fcs                       CD56_BV605(Beads).fcs
#> CD62L_BUV395(Beads).fcs                   CD62L_BUV395(Beads).fcs
#> CD69_BUV563(Beads).fcs                     CD69_BUV563(Beads).fcs
#> CD7_BV711(Beads).fcs                         CD7_BV711(Beads).fcs
#> CD8_BUV496(Beads).fcs                       CD8_BUV496(Beads).fcs
#> CXCR3_BUV737(Beads).fcs                   CXCR3_BUV737(Beads).fcs
#> Dump_CD14_PacificBlue(Beads).fcs Dump_CD14_PacificBlue(Beads).fcs
#> Dump_CD19_PacificBlue(Beads).fcs Dump_CD19_PacificBlue(Beads).fcs
#> IFNg_BV750(Beads).fcs                       IFNg_BV750(Beads).fcs
#> NKG2D_PE(Beads).fcs                           NKG2D_PE(Beads).fcs
#> PD1_PE-Vio770(Beads).fcs                 PD1_PE-Vio770(Beads).fcs
#> TNFa_PE-Dazzle594(Beads).fcs         TNFa_PE-Dazzle594(Beads).fcs
#> Va7.2_AlexaFluor647(Beads).fcs     Va7.2_AlexaFluor647(Beads).fcs
#> VD2_BUV661(Beads).fcs                       VD2_BUV661(Beads).fcs

NotBV786 <- Luciernaga_QC(x=MyBeadsGatingSet[2], desiredAF = "V7-A",
                    subsets="singlets", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "beads",
                    Unstained = FALSE, ratiopopcutoff = 0.001, Verbose = TRUE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> 0.19 of all events were negative and will be rounded to 0
#> Returning Peak Bead Detector Medians
#>   TheDetector   TheMedian
#> 1         V15 394556.2500
#> 2         YG1    768.0050
#> 3          V7    713.0750
#> 4         UV9    764.5007
#> 5          V5    765.1188
#> 6          V3    735.7625
#> 7          V4    719.6750
#>   Fluors Counts
#> 1    V15   1146
#> [1] "V15"
#> Normalizing Data for Signature Comparison
#>   Fluors Counts
#> 1    V15   1146

Because each of these noisy background signatures is so completely different from each other (unlike is the case with cells) we need to provision Luciernaga_QC() with a general unstained bead autofluorescence signature to the BeadAF signature to Luciernaga_QC() to enable external autofluorescence subtraction for the bead controls. Examples of how to do this are shown below:

MyUnstainedBeadsGatingSet
#> A GatingSet with 1 samples
UnstainedBeads <- Luciernaga_QC(x=MyUnstainedBeadsGatingSet[1], desiredAF = NULL,
                    subsets="singlets", removestrings=removestrings,
                    sample.name="GUID", unmixingcontroltype = "beads",
                    Unstained = TRUE, ratiopopcutoff = 0.001, Verbose = TRUE,
                    AFOverlap = AFOverlap, stats = "median",
                    ExportType = "data.frame", SignatureReturnNow = TRUE,
                    outpath = NULL)
#> 0.32 of all events were negative and will be rounded to 0
#> Returning Peak Bead Detector Medians
#>    TheDetector TheMedian
#> 1          YG1  757.4700
#> 2           V7  948.5438
#> 3          UV9  762.5669
#> 4           V5  819.8438
#> 5           V3  814.5500
#> 6           V8  773.3687
#> 7           V4  772.7500
#> 8           V6  792.4125
#> 9          UV7  768.3682
#> 10         UV2  768.2194
#> 11          R4  659.6800
#> 12          V2  744.3563
#> 13         V10  703.5875
#>   Fluors Counts
#> 1     V7    215
#> 2     V5    115
#> 3     V3     64
#> 4     V6     50
#> Normalizing Data for Signature Comparison
#> Returning designated stats values for Beads_Unstained, please return
#>     to LuciernagaQC as BeadsAF for subtraction for single color unmixing controls

UnstainedBeads
#>      UV1-A    UV2-A   UV3-A    UV4-A   UV5-A    UV6-A    UV7-A    UV8-A
#> 1 87.05594 113.8309 65.1525 98.58407 108.885 152.8406 214.1628 153.3984
#>      UV9-A   UV10-A   UV11-A   UV12-A   UV13-A   UV14-A   UV15-A   UV16-A
#> 1 317.5441 18.70531 84.89907 25.17594 21.97781 30.41938 20.04406 46.07532
#>       V1-A     V2-A     V3-A     V4-A V5-A     V6-A     V7-A     V8-A  V9-A
#> 1 135.1281 226.9438 339.5563 296.7594  363 323.6406 419.6844 295.8313 191.4
#>      V10-A   V11-A    V12-A    V13-A    V14-A    V15-A    V16-A    B1-A    B2-A
#> 1 230.3469 121.825 69.50625 69.19688 61.94375 64.69375 11.20625 220.545 136.175
#>       B3-A     B4-A     B5-A     B6-A  B7-A    B8-A    B9-A  B10-A B11-A  B12-A
#> 1 174.2325 124.9625 135.3625 107.5425 95.94 34.2875 73.9375 52.325 41.34 32.695
#>   B13-A  B14-A   YG1-A YG2-A  YG3-A  YG4-A YG5-A  YG6-A YG7-A YG8-A  YG9-A
#> 1 39.65 35.035 486.465 163.1 125.37 100.24 36.68 48.125 69.58 30.38 56.525
#>   YG10-A   R1-A    R2-A   R3-A    R4-A  R5-A  R6-A   R7-A   R8-A
#> 1      0 149.24 120.855 138.25 157.815 70.84 53.48 74.935 59.115

With this now provisioned, we can run single-color bead unmixing controls through Luciernaga_QC() in similar manner as the cell single-color unmixing controls. We also need to provide a stand-in argument for main autofluorescence detector (BeadMainAF=“UV1-A”) even though the detector is not used (something I will update out going forward)

StorageLocation <- file.path("C:", "Users", "12692", "Desktop")

SingleStain_Signatures <- map(.x=MyBeadsGatingSet[2], .f=Luciernaga_QC, subsets="singlets",
                              removestrings=removestrings, sample.name="GUID",
                              unmixingcontroltype = "cells", Unstained = FALSE,
                              ratiopopcutoff = 0.001, Verbose = FALSE,
                              AFOverlap = AFOverlap, stats = "median",
                              ExportType = "data", SignatureReturnNow = FALSE,
                              outpath = StorageLocation, Increments=0.1,
                              SecondaryPeaks=2, experiment = "UnstainedSignature",
                              condition = "Test", SCData="subtracted",
                              NegativeType="default", BeadAF=UnstainedBeads,
                              BeadMainAF="UV1-A")

gt(SingleStain_Signatures[[1]])
Sample Experiment Condition Cluster Count UV1 UV2 UV3 UV4 UV5 UV6 UV7 UV8 UV9 UV10 UV11 UV12 UV13 UV14 UV15 UV16 V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 YG1 YG2 YG3 YG4 YG5 YG6 YG7 YG8 YG9 YG10 R1 R2 R3 R4 R5 R6 R7 R8
CCR6_BV786(Beads) UnstainedSignature Test V15_10-UV16_02 1126 0.1 0.1 0.1 0.10 0.1 0.10 0.1 0.10 0.1 0.10 0.1 0.1 0.1 0.1 0.20 0.2 0.10 0.1 0.1 0.1 0.10 0.1 0.1 0.1 0.1 0.10 0.10 0.1 0.1 0.30 1 0.6 0.10 0.1 0.10 0.1 0.10 0.1 0.10 0.10 0.10 0.10 0.10 0.1 0.1 0.1 0.10 0.10 0.1 0.10 0.1 0.10 0.1 0.10 0.1 0.10 0.1 0.1 0.1 0.1 0.10 0.1 0.10 0.1
CCR6_BV786(Beads) UnstainedSignature Test V15_10-UV16_00 14 0.0 0.0 0.1 0.05 0.0 0.05 0.0 0.05 0.0 0.05 0.2 0.1 0.2 0.1 0.05 0.0 0.05 0.0 0.1 0.1 0.05 0.0 0.0 0.0 0.0 0.05 0.15 0.0 0.0 0.15 1 0.3 0.05 0.0 0.05 0.1 0.25 0.0 0.15 0.05 0.05 0.05 0.05 0.0 0.0 0.1 0.05 0.05 0.0 0.05 0.1 0.05 0.0 0.05 0.1 0.05 0.1 0.1 0.2 0.0 0.05 0.2 0.05 0.0

Beads are not subject to the AFOverlap.csv, are set to use Subtraction=“External” by default. Consequently, they will accept SCData of “raw” or “subtracted”, and NegativeType = “default”, “artificial”, “sample” in a similar manner as was described in detail for the single-color cell unmixing controls for their export arguments.

StorageLocation <- file.path("C:", "Users", "JohnDoe", "Desktop")

SingleStain_Signatures <- map(.x=MyBeadsGatingSet[2], .f=Luciernaga_QC, subsets="singlets",
                              removestrings=removestrings, sample.name="GUID",
                              unmixingcontroltype = "cells", Unstained = FALSE,
                              ratiopopcutoff = 0.001, Verbose = FALSE,
                              AFOverlap = AFOverlap, stats = "median",
                              ExportType = "fcs", SignatureReturnNow = FALSE,
                              outpath = StorageLocation, Increments=0.1,
                              SecondaryPeaks=2, experiment = "UnstainedSignature",
                              condition = "Test", SCData="subtracted",
                              NegativeType="default", BeadAF=UnstainedBeads,
                              BeadMainAF="UV1-A")

Conclusion

You have made it through the most complicated of the Luciernaga package functions. I hope this helps explain the function that makes the next two vignettes possible when scaled to process all the specimens within the GatingSet. Congratulations!

#> R version 4.4.1 (2024-06-14 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 22631)
#> 
#> Matrix products: default
#> 
#> 
#> locale:
#> [1] LC_COLLATE=English_United States.utf8 
#> [2] LC_CTYPE=English_United States.utf8   
#> [3] LC_MONETARY=English_United States.utf8
#> [4] LC_NUMERIC=C                          
#> [5] LC_TIME=English_United States.utf8    
#> 
#> time zone: America/New_York
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#>  [1] htmltools_0.5.8.1    plotly_4.10.4        gt_0.11.1           
#>  [4] stringr_1.5.1        purrr_1.0.2          dplyr_1.1.4         
#>  [7] data.table_1.16.2    ggcyto_1.32.0        ncdfFlow_2.50.0     
#> [10] BH_1.84.0-0          ggplot2_3.5.1        openCyto_2.16.1     
#> [13] flowWorkspace_4.16.0 flowCore_2.16.0      Luciernaga_0.99.1   
#> [16] BiocStyle_2.32.1    
#> 
#> loaded via a namespace (and not attached):
#>  [1] RBGL_1.80.0           gridExtra_2.3         rlang_1.1.4          
#>  [4] magrittr_2.0.3        matrixStats_1.4.1     ggridges_0.5.6       
#>  [7] compiler_4.4.1        dir.expiry_1.12.0     png_0.1-8            
#> [10] systemfonts_1.1.0     vctrs_0.6.5           reshape2_1.4.4       
#> [13] pkgconfig_2.0.3       fastmap_1.2.0         labeling_0.4.3       
#> [16] utf8_1.2.4            rmarkdown_2.28        graph_1.82.0         
#> [19] ragg_1.3.3            xfun_0.48             zlibbioc_1.50.0      
#> [22] cachem_1.1.0          jsonlite_1.8.9        SnowballC_0.7.1      
#> [25] highr_0.11            parallel_4.4.1        R6_2.5.1             
#> [28] bslib_0.8.0           stringi_1.8.4         RColorBrewer_1.1-3   
#> [31] reticulate_1.39.0     lubridate_1.9.3       jquerylib_0.1.4      
#> [34] figpatch_0.2          Rcpp_1.0.13           bookdown_0.41        
#> [37] knitr_1.48            zoo_1.8-12            Matrix_1.7-0         
#> [40] timechange_0.3.0      tidyselect_1.2.1      rstudioapi_0.17.0    
#> [43] yaml_2.3.10           viridis_0.6.5         lattice_0.22-6       
#> [46] tibble_3.2.1          plyr_1.8.9            Biobase_2.64.0       
#> [49] basilisk.utils_1.16.0 withr_3.0.1           evaluate_1.0.1       
#> [52] Rtsne_0.17            desc_1.4.3            xml2_1.3.6           
#> [55] pillar_1.9.0          lsa_0.73.3            BiocManager_1.30.25  
#> [58] filelock_1.0.3        stats4_4.4.1          generics_0.1.3       
#> [61] S4Vectors_0.42.1      munsell_0.5.1         scales_1.3.0         
#> [64] glue_1.8.0            lazyeval_0.2.2        tools_4.4.1          
#> [67] hexbin_1.28.4         fs_1.6.4              XML_3.99-0.17        
#> [70] grid_4.4.1            flowClust_3.42.0      tidyr_1.3.1          
#> [73] RProtoBufLib_2.16.0   crosstalk_1.2.1       colorspace_2.1-1     
#> [76] patchwork_1.3.0       basilisk_1.16.0       cli_3.6.3            
#> [79] textshaping_0.4.0     fansi_1.0.6           cytolib_2.16.0       
#> [82] viridisLite_0.4.2     uwot_0.2.2            Rgraphviz_2.48.0     
#> [85] gtable_0.3.5          sass_0.4.9            digest_0.6.37        
#> [88] BiocGenerics_0.50.0   htmlwidgets_1.6.4     farver_2.1.2         
#> [91] pkgdown_2.1.1         lifecycle_1.0.4       httr_1.4.7