Skip to contents

Introduction

Unmixing is a black box for many spectral flow cytometry users, you adjust gates on your single color controls, you provide full-stained samples, you unmix and then you evaluate the outputs with NxN plots. The golden rules of reference controls (1. Single Color Controls as Bright or Brighter than Full-Stain Sample; 2. Unmixing Single Color should be the same fluorophore (even better same manufacturer and lot); 3. Single Color Controls should have autofluorescence subtracted from a matching/equivalent unstained sample; 4. enough events) are useful guidepost that obviously work but few have mechanistic explanations behind why.

Building on examples from Jakob Theorell’s flowSpecs and Christopher Hall’s flowUnmix package, we implemented a way to take Luciernaga_QC() outputs of purified fluorophore signatures and unmix them using ordinal least squares (OLS) working from GatingSet objects, and returning FCS 3.0 standard files. In combination with functional programming principles, we have been leveraging this to understand how variations of fluorophore signature and brightness impact the unmixing of full-stained samples. We hope our addditive contribution enables users to push the limits of SFC and uncover new insights, write ways to handle issues arising from relative heterogeneity of individual immune cells unmixed with combination outputs, and spare future graduate students having to go write their own R package to answer space-wormhole questions.

Getting Started

This section uses the generated purified flourophore signatures generated by Luciernaga_QC() in the previous vignettes.

Let’s first load the required packages by calling them with library.

Then we can find the .fcs files stored within the Luciernaga packages extdata folder and sort them by their respective type

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"
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)]

Now lets create a GatingSet for our single-color cell unmixing controls

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

Now lets create a GatingSet for our unstained cell unmixing controls

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

Generate Luciernaga_QC Outputs

Now that the GatingSets are re-established, let’s continue where the last vignette left off by processing all the fcs files with Luciernaga_QC to characterize the fluorescent signatures within.

Let’s first provision the AFOverlap csv to handle conflicts.

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

And next generate a CellAF unstained signature that can be used when these fluorophore-autofluorescence overlap files are encountered:

# pData(MyUnstainedGatingSet[1])
removestrings <- c(".fcs")

TheCellAF <- map(.x=MyUnstainedGatingSet[1], .f=Luciernaga_QC, subsets="lymphocytes",
                              removestrings=removestrings, sample.name="GUID",
                              unmixingcontroltype = "cells", Unstained = TRUE,
                              ratiopopcutoff = 0.001, Verbose = FALSE,
                              AFOverlap = AFOverlap, stats = "median",
                              ExportType = "data", SignatureReturnNow = TRUE,
                              outpath = TemporaryFolder, Increments=0.1,
                              SecondaryPeaks=2, experiment = "FirstExperiment",
                              condition = "ILTPanel", SCData="subtracted",
                              NegativeType="default")
#> Normalizing Data for Signature Comparison

TheCellAF <- TheCellAF[[1]] #Removes list caused by map

gt(TheCellAF)
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
359.38 612.9988 437.0275 559.4488 779.7476 1246.153 2423.138 1793.776 1682.139 625.345 402.5919 251.5363 198.8788 255.1806 175.5994 168.3106 379.9125 1167.719 1815.069 2080.031 3214.613 3112.45 4226.681 3096.637 2104.025 2316.6 1251.25 704.825 632.8438 574.3375 518.1688 295.5562 1192.289 1367.454 1904.727 1289.431 1090.77 870.5431 619.2231 459.5087 477.0831 316.0812 239.0244 221.5788 163.0619 210.1844 516.2194 394.8131 402.2363 388.5 308.8575 310.3838 380.73 196.4006 195.2213 131.2575 189.9812 239.6306 253.4731 285.1131 152.1969 142.1681 129.8794 89.97625

Now let’s use Luciernaga_QC() with ExportType = “fcs” to export the data as individual .fcs files, and set Brightness = TRUE to save .csv files that can be used for Luciernaga_Tree(). For this vignette, we will be saving the .fcs files to a temporary folder. On your own workstation, save the outputs to a folder where you can retrieve them later by providing a file.path to the outpath argument.

Let’s start by processing the single-color unmixing controls.

#pData(MyGatingSet)

StorageLocation <- file.path(tempdir(), "LuciernagaOutputs")

if (!dir.exists(StorageLocation)) {
  dir.create(StorageLocation)
}

SingleColor_Data  <- map(.x=MyGatingSet, .f=Luciernaga_QC, subsets="nonDebris",
                              removestrings=removestrings, sample.name="GUID",
                              unmixingcontroltype = "cells", Unstained = FALSE,
                              ratiopopcutoff = 0.001, Verbose = FALSE,
                              AFOverlap = AFOverlap, stats = "median",
                              ExportType = "fcs", Brightness=TRUE, SignatureReturnNow = FALSE,
                              outpath = StorageLocation, Increments=0.1,
                              SecondaryPeaks=2, experiment = "FirstExperiment",
                              condition = "ILTPanel", Subtraction = "Internal", 
                              CellAF=TheCellAF, SCData="subtracted",
                              NegativeType="default", minimalfcscutoff=0.01)
#> No second peak
#> No second peak
#> No second peak
#> No second peak
#> 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.
#> No second peak
#> No second peak
#> No second peak
#> No second peak
#> No second peak
#> No second peak

TheLuciernagaOutputs_FCS <- list.files(StorageLocation, pattern="fcs", full.names = TRUE)
head(TheLuciernagaOutputs_FCS, 4)
#> [1] "C:\\Users\\12692\\AppData\\Local\\Temp\\Rtmp0AUE9A/LuciernagaOutputs/CCR4_BUV615(Cells)_UV1010YG300V1000.fcs"
#> [2] "C:\\Users\\12692\\AppData\\Local\\Temp\\Rtmp0AUE9A/LuciernagaOutputs/CCR4_BUV615(Cells)_UV1010YG307V1000.fcs"
#> [3] "C:\\Users\\12692\\AppData\\Local\\Temp\\Rtmp0AUE9A/LuciernagaOutputs/CCR4_BUV615(Cells)_UV1010YG307V1003.fcs"
#> [4] "C:\\Users\\12692\\AppData\\Local\\Temp\\Rtmp0AUE9A/LuciernagaOutputs/CCR4_BUV615(Cells)_UV1010YG307V1004.fcs"

And let’s also process an unstained unmixing control specimen also to characterize autofluorescence that is present.

#pData(MyUnstainedGatingSet)
Unstained_Data  <- map(.x=MyUnstainedGatingSet[1], .f=Luciernaga_QC, subsets="nonDebris",
                              removestrings=removestrings, sample.name="GUID",
                              unmixingcontroltype = "cells", Unstained = TRUE,
                              ratiopopcutoff = 0.001, Verbose = FALSE,
                              AFOverlap = AFOverlap, stats = "median",
                              ExportType = "fcs", Brightness=TRUE, SignatureReturnNow = FALSE,
                              outpath = StorageLocation, Increments=0.1,
                              SecondaryPeaks=2, experiment = "FirstExperiment",
                              condition = "ILTPanel", Subtraction = "Internal", 
                              CellAF=TheCellAF, SCData="subtracted",
                              NegativeType="default", minimalfcscutoff=0.01)

Luciernaga_Tree

We generate a lot of clusters with Luciernaga_QC(). We can visualize them using the report and plotting functions described in the previous vignette, but selecting individual candidate output .fcs for use in unmixing can be tiresome and confusing. Luciernaga_Tree() is our initial attempt to reduce the burden, by instituting a decision tree to help filter the many outputs and return likely candidates that will work for unmixing. It relies on the Luciernaga_QC() Brightness=TRUE .csv outputs in making this decision.

We want to be upfront and say this is developmental. We have recently created the tools to allow us to query how fluorophore brightness, signature, and relative abundance impact the unmixing of full-stained samples. We have not had the time to delve into the outcomes at the depth we would like to come up with a grand unified theory of perfect unmixing. That is on my to-do-list as a postdoc/industry/whatever (hire me if this interest you or you want to avoid me working for your competitors). For the time, it works well enough, but with occasional bugs in the final unmixing. We highly encourage your feedback and tinkering with the decision trees step methodology in order to achieve more consistent results. For now, the process works as follows (detailed explanation logic).

With that background out of the way, let’s continue.

ReferencePath <- system.file("extdata", package = "Luciernaga")
PanelPath <- file.path(ReferencePath, "UnmixingPanel.csv")
UnmixingPanel <- read.csv(PanelPath, check.names=FALSE)

MoveThese <- Luciernaga_Tree(BrightnessFilePath = StorageLocation, PanelPath = PanelPath)
gt(head(MoveThese, 5))
sample Cluster Decision Brightness Detector1 Detector1Value Detector2 Detector2Value Detector3 Detector3Value Detector1Raw Detector2Raw Detector3Raw Count Ratio Time UV1 UV2 UV3 UV4 UV5 UV6 UV7 UV8 UV9 UV10 UV11 UV12 UV13 UV14 UV15 UV16 SSC-W SSC-H SSC V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 FSC-W FSC-H FSC SSC-B-W SSC-B-H SSC-B 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
CD62L_BUV395(Cells) UV2_10-V7_04-B3_02 Fourth Level 16 UV2 10 V7 4 B3 2 16802.65 5682.600 2461.571 100 0.02515723 334183 4784.4324 16802.6517 8266.559 6514.1719 5815.1582 6467.6503 5803.6301 3065.7003 2504.0204 793.0607 527.2444 239.1900 220.3731 276.7122 225.5050 47.0050 753165.5 2305322 2857364.8 538.10629 1925.58435 2992.4813 3250.5686 4505.0500 4137.3751 5682.5996 4346.3407 2877.9094 3246.4780 1820.0875 1008.9063 815.13440 821.2188 570.693726 341.1375 702061.5 1752840 2062621 736991.8 1227946.5 1501699.9 1469.90649 1719.32745 2461.57123 1833.4966 1463.27600 1197.27835 851.6490 604.5134 619.3197 342.08875 400.73438 240.247498 160.808731 204.3262 856.78128 586.91249 619.9004 780.5728 554.4797 577.0959 670.6829 309.79408 175.13719 121.5797 736.05373 1107.39996 949.3766 842.2031 524.92029 430.989 352.98373 229.21343
CD8_BUV496(Cells) UV7_10-V6_02 Second Level 12 UV7 10 V6 2 NA NA 70514.98 9989.169 NA 356 0.19614325 215615 643.8644 2160.2963 1777.116 1547.4463 2121.7701 12550.8562 70514.9775 37302.5232 17475.8574 4163.3640 1116.9266 494.4078 309.6975 324.9072 165.4100 149.3450 714194.6 1264984 1482504.8 0.00000 0.00000 437.3188 4245.9658 13527.0786 9989.1694 9940.2881 3753.2686 1716.3438 1295.8689 199.3064 62.5968 70.64062 0.0000 9.831238 0.0000 687766.3 1457523 1650837 696512.3 610634.0 695478.2 4402.12341 2587.23120 1946.73193 382.9025 187.13818 35.05212 0.0000 0.0000 0.0000 0.00000 0.00000 0.000000 1.416245 0.0000 0.00000 0.00000 0.0000 0.0000 0.0000 0.0000 0.0000 0.00000 0.00000 0.0000 0.00000 0.00000 0.0000 0.0000 0.00000 0.000 0.00000 0.00000
CD69_BUV563(Cells) UV9_10-YG1_05-B4_02 Fourth Level 17 UV9 10 YG1 5 B4 2 58291.45 24073.820 10734.081 2557 0.48483125 290718 450.6753 1821.7785 1427.554 1217.5560 1194.4254 1296.5423 1061.2197 19671.4825 58291.4473 14812.1540 3321.2531 1374.3013 721.0656 485.2597 286.5669 113.9425 676148.9 814268 918510.8 0.00000 0.00000 0.0000 0.0000 0.0000 0.0000 636.5564 3010.4248 1032.5907 732.8405 0.0000 0.0000 0.00000 0.0000 0.000000 0.0000 665055.9 1180244 1308017 664419.1 391099.0 433493.0 0.00000 0.00000 5128.04797 10734.0806 5463.21667 3337.10327 915.7665 527.0060 392.0116 110.88593 48.66751 9.205612 0.000000 0.0000 24073.81958 11266.91614 6576.4381 1813.6359 964.4513 675.1575 461.5172 62.57625 21.50624 0.0000 0.00000 0.00000 0.0000 0.0000 0.00000 0.000 0.00000 0.00000
CCR4_BUV615(Cells) UV10_10-YG3_09-V10_03 Second Level 22 UV10 10 YG3 9 V10 3 11788.22 10077.725 2985.744 105 0.28301887 301599 163.4763 465.6991 296.905 335.6916 357.8181 443.3494 567.5186 448.5556 2767.7170 11788.2153 4934.8184 2550.1329 1411.9722 1318.7060 816.0426 357.2604 691204.0 942409 1078212.5 65.96564 171.42816 130.6594 138.1533 149.7719 145.0282 212.0938 285.2094 989.3125 2985.7437 947.3751 478.2250 262.24692 148.8438 110.893768 0.0000 670592.4 1333099 1482412 677919.3 432542.0 491643.7 0.00000 15.99713 93.40814 0.0000 402.24719 758.11221 357.5709 185.7218 245.3975 75.89815 21.72657 1.351883 13.100319 0.0000 847.51971 4367.43384 10077.7245 4452.3139 3342.9385 2586.9591 2417.1986 826.77660 584.55374 250.3744 63.52719 48.09562 102.7594 0.0000 16.03188 0.000 14.93718 12.46531
VD2_BUV661(Cells) UV11_10-R2_09-YG5_04 Fourth Level 23 UV11 10 R2 9 YG5 4 86813.77 70225.652 27538.511 162 0.43665768 273109 419.2147 1979.3049 1661.798 1488.8387 1489.6198 2034.1936 2082.1655 1109.2660 896.0701 4066.1930 86813.7747 40145.4697 30780.3556 25282.2200 13378.0181 8205.7942 676304.0 1042726 1192782.1 0.00000 77.61871 102.3344 189.4062 265.0658 238.7688 360.1470 362.3813 290.2969 1423.9502 23883.7852 10718.9844 8158.66602 5113.2468 3031.771851 1253.5188 668752.4 1442295 1588795 666351.9 469419.5 528043.8 23.56128 21.75873 41.16785 163.3515 72.22876 184.69186 3418.9240 2455.4234 1950.8199 1238.99338 788.59373 503.219345 347.753754 289.6231 41.93719 99.20624 1108.7166 39621.1736 27538.5113 18824.3518 21832.0009 7933.17007 4608.65071 2325.3459 68384.88156 70225.65219 52687.7684 37440.7836 26223.80379 14398.319 11817.25746 5413.54745

By scrolling through the returned selections, we can screen based on our knowledge of the panel and decide if the outcomes seem reasonable. We will also verify that these decisions were correct before we reach the unmixing process by visualizing vs. reference signatures in Luciernaga_SingleColors(). In the case that the wrong file output was selected, it is much easier to remove and replace one .fcs file than 30.

Luciernaga_Move

Once Luciernaga_Tree() has identified the Luciernaga_QC() output .fcs files that will likely produce the best unmixing outcome, it would be absolutely brutal having to track down within a folder of hundreds of .fcs files with some repetition of CCR4BUV615_UV6_10-V7_08-B3_04 nomenclature (believe me, I did so initially). Luciernaga_Move() takes the Luciernaga_Tree() list of ideal candidates, and copies these .fcs files to a designated folder, saving you the hassle, and allowing you to simply point at that folder for use in the functions mentioned below.

Continuing from where we left off with Luciernaga_Tree() above, for this example, we will create a different temporary folder were we will store the selected .fcs files for later use in the unmixing.

SortedStorageLocation <- file.path(tempdir(), "LuciernagaSelected")

if (!dir.exists(SortedStorageLocation)) {
  dir.create(SortedStorageLocation)
}

UnmixingPanel <- read.csv(PanelPath, check.names=FALSE)
TheseFluorophores <- UnmixingPanel %>% pull(Fluorophore)
walk(.x=TheseFluorophores, .f=Luciernaga_Move, data=MoveThese, input=StorageLocation, output=SortedStorageLocation)

MovedFiles <- list.files(SortedStorageLocation, pattern="fcs", full.names=TRUE)
length(MovedFiles)
#> [1] 30

With these steps completed, we are now ready to proceed to the steps to validate our choice in unmixing controls.

Luciernaga_LinearSlices

We had previously showcased Luciernaga_LinearSlices() ability to take an .fcs file, and visualize variation in the signature based on the quantile splits for the MFI brightness. What we saw for APC CD16 is replicated below:

#pData(MyGatingSet)
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 today, we highlight Luciernaga_LinearSlices() ability to do this also with the Luciernaga_QC() outputs.

MovedFiles <- list.files(SortedStorageLocation, pattern="fcs", full.names=TRUE)
Selected_CS <- load_cytoset_from_fcs(MovedFiles, truncate_max_range = FALSE, transform = FALSE)
Selected_GS <- GatingSet(Selected_CS)

pData(Selected_GS)
#>                                                                                          name
#> CCR4_BUV615(Cells)_UV1010YG309V1003.fcs               CCR4_BUV615(Cells)_UV1010YG309V1003.fcs
#> CCR6_BV786(Cells)_V1510UV1502.fcs                           CCR6_BV786(Cells)_V1510UV1502.fcs
#> CCR7_BV650(Cells)_V1110YG502R202.fcs                     CCR7_BV650(Cells)_V1110YG502R202.fcs
#> CD107a_APC-R700(Cells)_R110R210YG504.fcs             CD107a_APC-R700(Cells)_R110R210YG504.fcs
#> CD127_BV421(Cells)_V110V210UV701.fcs                     CD127_BV421(Cells)_V110V210UV701.fcs
#> CD16_APC(Cells)_R110R209YG406.fcs                           CD16_APC(Cells)_R110R209YG406.fcs
#> CD161_BV480(Cells)_V510UV703.fcs                             CD161_BV480(Cells)_V510UV703.fcs
#> CD25_PE-Cy5(Cells)_YG510B807R203.fcs                     CD25_PE-Cy5(Cells)_YG510B807R203.fcs
#> CD26_PerCP-Cy5.5(Cells)_B910YG606V1306.fcs         CD26_PerCP-Cy5.5(Cells)_B910YG606V1306.fcs
#> CD27_APC-Fire750(Cells)_R710YG903V1502.fcs         CD27_APC-Fire750(Cells)_R710YG903V1502.fcs
#> CD3_AlexaFluor488(Cells)_B210.fcs                           CD3_AlexaFluor488(Cells)_B210.fcs
#> CD3_AlexaFluor647(Cells)_R210YG502.fcs                 CD3_AlexaFluor647(Cells)_R210YG502.fcs
#> CD3_SparkBlue550(Cells)_B310V703.fcs                     CD3_SparkBlue550(Cells)_B310V703.fcs
#> CD38_APC-Fire810(Cells)_R810YG1003.fcs                 CD38_APC-Fire810(Cells)_R810YG1003.fcs
#> CD4_BUV805(Cells)_UV1610R802.fcs                             CD4_BUV805(Cells)_UV1610R802.fcs
#> CD45RA_BV510(Cells)_V710UV804.fcs                           CD45RA_BV510(Cells)_V710UV804.fcs
#> CD56_BV605(Cells)_V1010YG304UV1003.fcs                 CD56_BV605(Cells)_V1010YG304UV1003.fcs
#> CD62L_BUV395(Cells)_UV210V704B302.fcs                   CD62L_BUV395(Cells)_UV210V704B302.fcs
#> CD69_BUV563(Cells)_UV910YG105B402.fcs                   CD69_BUV563(Cells)_UV910YG105B402.fcs
#> CD7_BV711(Cells)_V1310R403UV1402.fcs                     CD7_BV711(Cells)_V1310R403UV1402.fcs
#> CD8_BUV496(Cells)_UV710V602.fcs                               CD8_BUV496(Cells)_UV710V602.fcs
#> CXCR3_BUV737(Cells)_UV1410R506V704.fcs                 CXCR3_BUV737(Cells)_UV1410R506V704.fcs
#> Dump_CD19_PacificBlue(Cells)_V810V710UV806.fcs Dump_CD19_PacificBlue(Cells)_V810V710UV806.fcs
#> IFNg_BV750(Cells)_V1410V1509UV1502.fcs                 IFNg_BV750(Cells)_V1410V1509UV1502.fcs
#> INF071_Ctrl_Unstained_V710UV804B304.fcs               INF071_Ctrl_Unstained_V710UV804B304.fcs
#> NKG2D_PE(Cells)_YG110B407V802.fcs                           NKG2D_PE(Cells)_YG110B407V802.fcs
#> PD1_PE-Vio770(Cells)_YG910B1306V702.fcs               PD1_PE-Vio770(Cells)_YG910B1306V702.fcs
#> TNFa_PE-Dazzle594(Cells)_YG310B607V1002.fcs       TNFa_PE-Dazzle594(Cells)_YG310B607V1002.fcs
#> VD2_BUV661(Cells)_UV1110R209YG504.fcs                   VD2_BUV661(Cells)_UV1110R209YG504.fcs
#> Viability_ZombieNIR(Cells)_R710R610B1303.fcs     Viability_ZombieNIR(Cells)_R710R610B1303.fcs

ThePlots <- map(.x=Selected_GS, .f=Luciernaga_LinearSlices, subset="root", removestrings=".fcs",
                sample.name="GUID", stats="median", returntype="normalized", output="plot")

plotly::ggplotly(ThePlots[[1]])

As we can see, with the exception of cells below 30 percentile in brightness, most of the variation in signature we saw in the original file, and that the sorting within Luciernaga_QC() appears to have sorted cells of similar signature regardless of brightness.

By passing the generated plots to Utility_Patchwork(), we can set returntype=“pdf” or “patchwork” to generate a report for all fluorophores.

PatchworkObjects <- Utility_Patchwork(ThePlots, filename = "LinearSlices", outfolder=ReferencePath, returntype = "patchwork")
PatchworkObjects[1]
#> $`1`

Luciernaga_SingleColors

The main purpose of Luciernaga_SingleColors() is to generate a reference matrix for use in unmixing full-stained samples. It additionally provides a mechanism by which we can visualize the signatures we plan on using and compare them to the reference signatures stored within Luciernaga. This allows us to screen out potential issues in the single-color reference matrix before we proceed to unmix the full-stained specimens.

To begin, we need to generate either a .csv file or a data.frame for each of the fluorophores present, and specify a cutoff point for each, dictated by our observations from what we saw with Luciernaga_LinearSlices(). For this example, we will do this in R and set the intervals from 0.4 to 1.0 across the board. If you want to fine-tune things further, it would be easier to save the output as a .csv file, modify it, and then return it to R.

UnmixingPanel <- read.csv(PanelPath, check.names=FALSE)
ThePanelCuts <- UnmixingPanel %>% select(-Detector) %>% mutate(From=0.3) %>% mutate(To=1)
head(ThePanelCuts, 5)
#>   Fluorophore From To
#> 1    BUV395-A  0.3  1
#> 2    BUV496-A  0.3  1
#> 3    BUV563-A  0.3  1
#> 4    BUV615-A  0.3  1
#> 5    BUV661-A  0.3  1
#write.csv(ThePanelCuts, path="SaveHere.csv", row.names=FALSE)

An important thing to ensure is that we correctly identify each fluorophore and ligand at this step, to ensure names are correct in the unmixed full-stained .fcs file. In this case, we are using the keyword “TUBENAME” to identify between the .fcs files. This is what the original name looks like:

keyword(Selected_GS[1], "TUBENAME")
#>                 TUBENAME
#> 1 DR_CCR4 BUV615 (Cells)

Luciernaga_SingleColors() will automatically clean up any portion of the name that has “(Cells)” or “(Beads)” present in it. However, that would leave the final name as “DR_CCR4 BUV615” which would be converted into Fluorophore = BUV615, Ligand = DR_CCR4. To clean this up, we will remove the authors initials (“DR_”) by providing them as part of a list to the remove strings argument.

#pData(Selected_GS)
removestrings=c("DR_", ".fcs")
SCs <- map(.x=Selected_GS[1], .f=Luciernaga_SingleColors, sample.name="TUBENAME",
           removestrings=removestrings, subset="root", PanelCuts=ThePanelCuts,
           stats="median", Verbose=TRUE, SignatureView=TRUE, returntype = "plots")
#> After removestrings cleanup the name is CCR4 BUV615
#> The Fluorophore is BUV615 and the ligand is CCR4

In the case above, we set the returntype = “plot” to visualize the outcome compared to the stored Reference Signatures within Luciernaga. Let’s see what these look like:

plotly::ggplotly(SCs[[1]])

As you can tell, the provided signature originating from our output .fcs file closely resembles that of the reference signature. We can repeat this for all the fluorophores and using Utility_Patchwork(), set returntype = “pdf” and examine all fluorophores to spot any issues. For this example, I will set it the argument to “patchwork” to visualize.

SCs <- map(.x=Selected_GS, .f=Luciernaga_SingleColors, sample.name="TUBENAME",
           removestrings=removestrings, subset="root", PanelCuts=ThePanelCuts,
           stats="median", Verbose=TRUE, SignatureView=TRUE, returntype = "plots")

TheView <- Utility_Patchwork(x=SCs, filename="ReferenceMatches",
                             outfolder = ReferencePath, returntype = "patchwork")

TheView[3]
#> $`3`

In this particular case, we can see that while most generated signatures aligned with their references, the .fcs files we are using for BUV737 and PacificBlue deviate substantially and are likely to impact the final unmixing. Within our workflow, we would follow up by removing the current output .fcs from the Selected Folder, and replace it with another variant.

While this highlights the visualizing portion of Luciernaga_SingleColors() lets go ahead and generate the reference matrix by changing returntype = “data”

SC_Reference <- map(.x=Selected_GS, .f=Luciernaga_SingleColors, sample.name="TUBENAME",
                    removestrings=removestrings, subset="root", PanelCuts=ThePanelCuts,
                    stats="median", Verbose=FALSE, SignatureView=FALSE, returntype = "data") %>%
                    bind_rows()
head(SC_Reference, 4)
#>   Fluorophore Ligand     UV1-A       UV2-A      UV3-A     UV4-A     UV5-A
#> 1      BUV615   CCR4  186.1606  641.521606  390.39441  452.4603  452.2001
#> 2       BV786   CCR6   53.8475    7.325958   59.87189    0.0000    0.0000
#> 3       BV650   CCR7  593.7357 1046.530701  908.34186 1106.6628 1231.2781
#> 4    APC-R700 CD107a 1279.4733 3029.070801 3013.52637 3407.7139 4471.2764
#>       UV6-A      UV7-A     UV8-A    UV9-A    UV10-A    UV11-A   UV12-A
#> 1  557.8869   802.7666  660.1525 3567.620 14820.930  6095.664 3081.617
#> 2    0.0000     0.0000    0.0000    0.000     0.000     0.000    0.000
#> 3 1933.8989  3245.7625 2545.9304 2495.542  2563.000 10016.305 4557.254
#> 4 7032.0078 11404.7373 7828.4893 6751.242  2659.576 12536.651 5248.198
#>       UV13-A   UV14-A    UV15-A   UV16-A       V1-A      V2-A       V3-A
#> 1 1898.16150 1681.135  984.2044  508.316   19.35312  193.6343   141.0407
#> 2   10.70999 1352.324 5023.2507 4109.182 1641.57812 1643.1250  1479.6719
#> 3 3401.24329 2766.304 1604.9755 1068.918 4004.13745 5897.5815  6977.1631
#> 4 6263.04443 6465.568 3412.2507 2374.199 2380.12524 7611.5874 11858.7568
#>         V4-A       V5-A       V6-A       V7-A       V8-A     V9-A     V10-A
#> 1   138.1533   203.8094   145.0282   238.7686   382.6969 1327.975  3955.188
#> 2   641.8844   243.9250   106.8031     0.0000     0.0000    0.000     0.000
#> 3  5040.4067  5719.1064  5486.0781  7418.2969  5893.8003 5118.094 20654.357
#> 4 12383.5254 17391.3438 16034.9072 21225.4629 15140.5371 9949.020 11313.088
#>       V11-A      V12-A      V13-A     V14-A      V15-A     V16-A        B1-A
#> 1  1349.906   634.4249   358.0844   217.250   161.4937     0.000    2.542908
#> 2     0.000     0.0000   395.0031  7721.519 32902.4453 17867.919    0.000000
#> 3 58440.148 26664.5850 19260.8643 11865.803  7511.5566  3044.388 1887.603638
#> 4 36744.539 15177.9385 18939.9375 13898.775  8024.9131  3463.350 5915.804688
#>         B2-A       B3-A     B4-A      B5-A      B6-A       B7-A      B8-A
#> 1   17.47784   83.17242    0.000  526.4265  960.6359   424.8428  264.1306
#> 2    0.00000    0.00000    0.000    0.0000    0.0000     0.0000    0.0000
#> 3 2249.52002 3330.79468 2431.186 1948.3414 1809.3558  1888.2152 1424.3934
#> 4 6264.07324 8485.97656 5455.523 4460.5435 3486.8074 11263.1777 7628.5659
#>        B9-A      B10-A      B11-A      B12-A       B13-A       B14-A    YG1-A
#> 1  331.4025   82.72188   51.91843   24.26936    1.062187    7.885941 1144.792
#> 2    0.0000    0.00000   59.73999  231.42810  592.089050  648.224030    0.000
#> 3 1301.8878  809.12939  600.07156  424.45654  350.554016  341.123138 1052.800
#> 4 6682.5752 5642.40430 4194.80371 2879.23633 1801.791870 1890.049805 1597.429
#>          YG2-A     YG3-A     YG4-A     YG5-A     YG6-A     YG7-A      YG8-A
#> 1 5.777896e+03 12523.749  5701.758  4401.185  3108.451  2949.235  1086.6554
#> 2 3.468323e-02     0.000     0.000     0.000     0.000     0.000   165.0431
#> 3 9.510966e+02  2228.429 11140.862  7616.473  5853.169  6489.719  2278.3098
#> 4 1.466449e+03  2041.359 70883.148 47194.148 30333.732 57502.301 24562.9121
#>        YG9-A    YG10-A        R1-A        R2-A         R3-A         R4-A
#> 1   699.6469  335.9831    151.7378     41.5275 1.197094e+02     22.91782
#> 2   736.0688  612.6853      0.0000      0.0000 1.765671e-01     22.84718
#> 3  1590.9769  789.6956   6911.8921   6685.9980 5.755832e+03   3799.20117
#> 4 13512.3770 7128.1426 142743.1562 131535.3906 9.798209e+04 108904.66406
#>          R5-A       R6-A        R7-A         R8-A
#> 1    52.05063     0.0000    66.28156     2.224689
#> 2   197.25562   954.9206  2531.72974  1467.375549
#> 3  2560.01501  1486.2678  1330.36316   642.263794
#> 4 91088.17188 48160.9531 39263.82812 18772.619141

While we are at it, we might as well identify how far the cosine difference for the trouble Pacific Blue Fluorophore is before we go and correct it:

PacificBlue <- SC_Reference %>% select(-Ligand) %>% rename(Sample=Fluorophore)

Results <- QC_WhatsThis(x="Pacific Blue", data=PacificBlue, NumberHits=10,
                        returnPlots = TRUE)
#> Normalizing Data for Signature Comparison

Results[[1]]
#>         Fluorophore ID_Pacific Blue
#> 1             BV510            0.88
#> 2    LIVE DEAD Aqua            0.84
#> 3          VioGreen            0.82
#> 4      Krome Orange            0.80
#> 5             OC515            0.80
#> 6       Zombie Aqua            0.80
#> 7  Monochlorobimane            0.79
#> 8   Amethyst Orange            0.78
#> 9             BV480            0.78
#> 10      cFluor V547            0.78
plotly::ggplotly(Results[[2]])

As you can tell, the fluorophore more closely resembles BV510 with no Pacific Blue appearing in the list of hits. This suggest that the returned fluorescent signature in the .fcs file is mainly autofluorescence, and should not be used.

Back on topic, once we have corrected the Selected Folder, rerun Luciernaga_SingleColors() and are satisfied with out results, we should go ahead and save the results data.frame elsewhere for further reference, or that they can be so edited to correct for any typos or format issues that may have been missed before creating unmixed .fcs files.

SC_Reference <- SC_Reference %>%
  mutate(Fluorophore = case_when(Fluorophore == "Unstained" ~ "AF", TRUE ~ Fluorophore))

TheSCReferences <- file.path(ReferencePath, "UnmixingSCs.csv")

write.csv(SC_Reference, TheSCReferences, row.names=FALSE)

Luciernaga_Unmix

Luciernaga_Unmix() is the unmixing function implemented within the Luciernaga package. What mainly distinguishes it from other R package implementations of ordinary least squares unmixing is it works at the GatingSet level in terms of infrastructure (reducing active memory use) and is set up in such a way to allow us to rapidly iterate/modify/change the inputs to subsequently evaluate the unmixed full-stained .fcs files for the impacts that those decisions had on the unmixing. We have put some effort into ensuring that the subsequent unmixed files are compatible with various software typically used by those who prefer to use GUI for their flow data. This involved changes done within the newly produced .fcs files exprs, parameters and description folder, it’s possible we may have missed something, so if you encounter a bug, please reach out.

As previously stated, this remains experimental, and at the moment is just intended as a tool to allow me to querry how brightness/signature/abundance of individual single colors impacts the full-unmixing. As I improve on my existing knowledge, the quality of inputs/outputs is likely to also improve as I figure out the things that I don’t yet know and correct for them. So consider this a work in progress for now, feel free to reach out if you know something I don’t, and want to collaborate on getting it implemented here.

For now, let’s identify the raw full-stained files and load them into a GatingSet:

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)
RawFullStainedFCSFiles <- FCS_Files[grep("Tetramer", FCS_Files)]
RawFullStainedFCSFiles <- RawFullStainedFCSFiles[-grep("Unmixed", RawFullStainedFCSFiles)]

UnmixCytoSet <- load_cytoset_from_fcs(RawFullStainedFCSFiles, truncate_max_range = FALSE, transform = FALSE)
UnmixGatingSet <- GatingSet(UnmixCytoSet)                                                

Let’s identify the Single Color Reference Data output from Luciernaga_SingleColors() that we have validated (correcting from any issues)

ReferencePath <- system.file("extdata", package = "Luciernaga")
ValidatedSCReferenceData <- file.path(ReferencePath, "ValidatedSCReferenceData.csv")
SingleColorReference <- read.csv(ValidatedSCReferenceData, check.names = FALSE)

And finally, lets provide a file.path to the panel (to establish correct ordering of fluorophores in the final file)

PanelPath <- file.path(ReferencePath, "UnmixingPanel.csv")
PanelNames <- read.csv(PanelPath, check.names=FALSE)

With these pre-requisites prepared we can go ahead. For this example, we will merge the “GROUPNAME” and “TUBENAME” to form the final name. For the final file, we will use the addon argument to append “_Unmixed” at the end. As Ordinary Least Squares (OLS) returns values close to 0, the multiplier increases all values across the board, which allows the data to resemble that of other softwares when the bi-exponential transform is applied.

TheSampleName <- c("GROUPNAME", "TUBENAME")

UnmixSuccess <- map(.x=UnmixGatingSet, .f=Luciernaga_Unmix, controlData=SingleColorReference,
                    sample.name=TheSampleName, addon="_Unmixed", subset="root", removestrings=".fcs",
                    multiplier=50000, outpath=UnmixedOutpath, PanelPath=PanelPath, Verbose=FALSE)

Luciernaga_IterativeUnmix

This function is an extension of Luciernaga_Unmix() using the same inputs, with the added provision that you provide it a folder of variant of Luciernaga_QC() .fcs files for a single fluorophore of interest. Luciernaga_IterativeUnmix() will then proceed one by one through the files in that folder, process them individually and swap them in to the Reference Matrix, unmix the full-stain samples, and return the variant unmixed full-stain files to the outfolder. It will repeat this until everything is complete. What we will do subsequently, is use Utility_UnityPlots() and Utility_NxNPlots() and the workflow described in Vignette 1 to consolidate all the variant unmixed files and evaluate how the variation in that iterated single-color impacted the final unmixing.

IterativePath <- file.path(ReferencePath, "DifferentialPerCP")
removestrings <- c("DR_", ".fcs")
iterate_removestrings <- c("DR_", "(Cells)", ".fcs", " ", "PerCP-Cy5.5", "CD26", "_")
TheSampleName <- c("GROUPNAME", "TUBENAME") 


Luciernaga_IterativeUnmixing(IterativePath=IterativePath, iterate_removestrings=iterate_removestrings,
                               removestrings=removestrings, sample.name=TheSampleName, subset="root",
                               PanelCuts=ThePanelCuts, stats="median", Verbose=FALSE, SignatureView=FALSE,
                               FullStainedGS=UnmixGatingSet, controlData=SingleColorReference, multiplier=50000,
                               outpath=UnmixedOutpath, PanelPath=PanelPath)

Conclusion

And with that, we conclude our tour of the current state of the unmixing functions within the Luciernaga package. They remain a work in progress, and we welcome any contributions/insights/bug-reports to continue improving on them. This entire project arose when curious of how placing a positive gate on a single-color unmixing control would alter the unmixing of that file, and the sum of Luciernaga’s functions have been geared to allowing me to answer these questions so that no other graduate student will have to go through the horror of “it unmixed weird, no idea why” 20 years from now.

#> 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        highr_0.11           
#> [25] SnowballC_0.7.1       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