# Remove existing installation
remove.packages("flowGate")
# Install the new version
remotes::install_github("NKInstinct/flowGate") # If non-functional, try the class fork at "DavidRach/flowGate"Retrieving Signature Matrices

For the YouTube livestream schedule, see here
For screen-shot slides, click here
Background
During weeks twelve through fourteen of the Cytometry in R course, we primarily focus on how to work with raw Spectral Flow Cytometry (SFC) .fcs files. During Week 12, we learn how to extract out and characterize fluorescent signatures that are present in our unmixing controls (both single-color and unstained). This is followed up on Week 13 by learning how to compare these signatures with each other, allowing us to predict how they will interact in context of a full SFC panel. Finally, in Week 14, we take the fluorescent signatures we have derived from our unmixing controls and use them to unmix full-stained samples.
Due to the Cytometry in R’s course use of GitHub, we are not able to include the full-sized .fcs files due to individual file size sharing constraints. The datasets used during Week’s 12 through 14 were consequently downsampled to get around this issue. However, working with the complete datasets can provide additional insight (as well as more closely replicate the experience of working with your own datasets). This walk-through therefore contains the steps needed to both retrieve and process the original .fcs files that were used from the ImmPort repository.
In the Set Up for Signature Retrieval walk-through, we transformed, gated and verified the gate placement for each of our unmixing control GatingSet objects (Bead Single Colors, Cell Single Colors, Cell Unstained), before saving each as an intermediate .gs objects we could work with more readily in the future without needing to re-draw and re-adjust these gates. We will pick back up with these intermediates, and work towards retrieving the fluorescent signatures from each of the single-colors to incorporate into a signature matrix. This will be used not only for comparisons during Week 13, but also for use in unmixing the full-stained samples during Week 14.
This walk-through will share a lot of the content that is being covered in Week 12. However, Week 12 walk-through is more granularly focused on explaining the individual signature extraction to break open what for many in the community is a black-box. This walk-through is more focused on providing a faster method by which we can do this at scale for our experiments, in a manner closer to what we would experience using commercial software, while providing launching points for delving further into the aggregated single-cell normalized signatures that will be the focus of the next walk-through.
Walk-through
Checking package versions
As was mentioned in the last walk-through, we are using the developmental version of the flowGate package that contains the new functions needed to adjust individual gates for individual specimens. Due to the twice yearly release cycle of Bioconductor packages, the easiest way to take advantage of these new functions is to install this version of flowGate via GitHub.
If you have already done this in the last walk-through, you are all set. Otherwise, as always, it is easier if you first remove the R package, close and reopen Positron, then install the new version.
Once this is done, check what version is installed using the packageVersion() function. The version containing both the ggplot2/ggcyto bug fixes as well as the new gating functions should be any version starting at/after 1.31.1
packageVersion("flowGate")[1] '1.13.1'
Additionally, we will be using the most recent version of the Luciernaga package, which has new orchestration functions intended to make setting up fluorophore-specific gates for the correct single-color easier than it would be to specify each of these gates individually.
If you run packageVersion(), you should have version 0.99.10 or greater installed to take advantage of the orchestration functions that are needed for this walk-through.
packageVersion("Luciernaga")[1] '0.99.10'
If you ned to install the newest version, proceed and repeat the uninstall/reinstall steps below.
# Remove existing installation
remove.packages("Luciernaga")
# Install the new version
remotes::install_github("DavidRach/Luciernaga")Initial Set Up
Having verified we have the correct package versions installed, we can proceed to attach the packages needed to our local environment via the library() call.
library(flowWorkspace)As part of improvements to flowWorkspace, some behavior of
GatingSet objects has changed. For details, please read the section
titled "The cytoframe and cytoset classes" in the package vignette:
vignette("flowWorkspace-Introduction", "flowWorkspace")
library(flowGate)Loading required package: ggcyto
Loading required package: ggplot2
Loading required package: flowCore
Loading required package: ncdfFlow
Loading required package: BH
Registered polygon_gate
Registered span_gate
library(dplyr)
Attaching package: 'dplyr'
The following object is masked from 'package:ncdfFlow':
filter
The following object is masked from 'package:flowCore':
filter
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library(purrr)
library(stringr)
library(Luciernaga)With this done, lets re-specify the file paths to our Storage and Output locations.
#StorageLocation <- file.path("course", "SDY3080", "data", "SDY3080")
StorageLocation <- file.path("data", "SDY3080")
#OutputLocation <- file.path("course", "SDY3080", "outputs")
OutputLocation <- file.path("outputs")Next up, given that not everyones workstations will be set up in the same way, I recommend working through each GatingSet separately. This in turn should reduce the total amount of RAM being taken up at any one point of time.
Beads Single-Colors
Let’s start with our Bead Single Color Unmixing controls, which theoretically should be the easier examples to work with. To get started, letss reload the GatingSet from where we stored the intermediate .gs file. For easier handling, we will save under the same name as we had been using in the previous walkthrough.
BeadsStoredHere <- file.path(OutputLocation, "BeadsSCs_GS.gs")
BeadsSC_GatingSet <- load_gs(BeadsStoredHere)pData(BeadsSC_GatingSet)
Updating Metadata
From the metadata, we can see that all the bead single-color controls appear to have been acquired as part of the ILT_00 experiment. If we checked the methods/metadata for the SDY3080 study (or alternatively, the internal keywords for the individual .fcs files), we would find that these files were acquired on a Cytek Aurora 5-laser instrument.
Currently, within the file name, we have several bits of information that would be useful to extract out and place in their column (either in pData() or left as a stand-alone data.frame/tibble). This includes Fluorophore, Antigen, and control type (“Beads”). Seeing as we currently have just a name column, we can repeat the appending metadata steps that we have encountered previously, only needing to modify the RegEx syntax needed for the stringr package to retrieve the bits of information.
bead_pd <- pData(BeadsSC_GatingSet)
Updated_bead_pd <- bead_pd |> mutate(
raw = str_extract(name, "(?<=DR_)[^(]+(?=\\()"),
Antigen = raw |>
str_trim() |>
str_extract("^\\S+"),
Fluorophore = raw |>
str_trim() |>
str_remove("^\\S+\\s+"),
Type = name |>
str_extract("(?<=\\()([^)]+)(?=\\))"),
raw = NULL
)
Updated_bead_pd <- Updated_bead_pd |> relocate(Fluorophore, Antigen, Type, .after="name")Updated_bead_pd
While skimming this updated output, we can notice that both Fluorophore and Antigen ended up as ‘< NA >’’ for the 4BeadsUnstained sample (which didn’t conform to the same RegEx pattern seen by the other unmixing controls). Additionally, for this particular Cytek Aurora, the naming convention that was inputted for PE-Vio 770 and PE-Dazzle 594 differs, with no space occuring between Vio770 and Dazzle594. We can correct these small issues through use of the dplyr packages case_when() function in conjunction with the mutate() function.
Updated_bead_pd <- Updated_bead_pd |> mutate(
Antigen = case_when(
str_detect(name, "BeadsUnstained") ~ NA_character_,
.default = Antigen
),
Fluorophore = case_when(
str_detect(name, "BeadsUnstained") ~ "BeadUnstained",
.default = Fluorophore
)
)
Updated_bead_pd <- Updated_bead_pd |>
mutate(Fluorophore = case_when(
Fluorophore == "PE-Vio770" ~ "PE-Vio 770",
Fluorophore == "PE-Dazzle594" ~ "PE-Dazzle 594",
TRUE ~ Fluorophore
))Updated_bead_pd
With these adjustments now made, we can overwrite the existing metadata, replacing with our variant.
pData(BeadsSC_GatingSet) <- Updated_bead_pd
pData(BeadsSC_GatingSet)Template Set Up
With the metadata now updated, let’s step back and consider some important elements that are missing. Currently, our single-color unmixing controls are present in a relatively generic GatingSet (last common gate being “scatter”). From here, based on the individual single-color control, we will want to create an individual gate for what should be the peak detector for the given fluorophore. We will then want to be able to adjust this individual gate (to accord with best practices), at which point we can extract out the average fluorescent signature for the beads within that gate. However, since these are single-color controls, we will also need to subtract out a background signal for beads, so we will need to provide a designated Negative specimen for this step.
Lets start off by first retrieving back out our metadata, saving it as a new object Template.
Template <- pData(BeadsSC_GatingSet)Using mutate(), lets go ahead and create two new columns, one for Detector (left empty as denoted by ““), and Negative (providing value”BeadsUnstained” to all since there is only one unstained sample in this GatingSet)
Template <- Template |> mutate(Detector="", Negative="BeadUnstained")
Since this data originates from a 5-laser Cytek Aurora with 64, Luciernaga has fairly good coverage of the fluorophore signatures. We can iterate in the individual values found in the “Florophore” column, and retrieve the corresponding peak detector.
Template <- Template |> mutate(
Detector = map_chr(Fluorophore, \(f) DetectorGuess(x = f, NumberDetectors = 64))
)
Alternatively, if you are working with data from a different spectral flow cytometer manufacturer, you can always specify the correct detectors for the given fluorophore manually (making sure to provide matching syntax to what you would see in the colnames())
colnames(BeadsSC_GatingSet)
While you could use mutate() and case_when() to do this entirely programmatically, you can also use write.csv() to save out as a .csv file (and also to provide an intermediate point to fall back on).
StoreHere <- file.path(OutputLocation, "BeadMatch.csv")
write.csv(Template, StoreHere, row.names=FALSE)It is also a good idea to duplicate the file and rename it (in this case I called it “UpdatedBeadMatch”). This ensures that should your R script get run after you incorporated in edits by hand, these are not lost.
StoreHere <- file.path(OutputLocation, "UpdatedBeadMatch.csv")
BeadMatchTemplate <- read.csv(StoreHere, check.names=FALSE)
At this point, we have an assembled template, containing most of the needed information we will need to first create the correct peak detector gates for each single-color, and subsequently match to the sample (BeadsUnstained) from which the background for subtraction is set to be extracted.
Removing Duplicates
Before proceeding, lets double check that there are no single-color controls that are present twice in the GatingSet.
BeadMatchTemplate |> group_by(Fluorophore) |> mutate(Count=row_number()) |>
ungroup() |> filter(Count > 1) |> pull(Fluorophore) # Spot duplicated fluorophore references[1] "Pacific Blue" "FITC" "Alexa Fluor 647"
In this case, we have a few. Lets isolate them for for now and see whats up.
BeadMatchTemplate |> filter(Fluorophore %in% c("Pacific Blue", "FITC", "Alexa Fluor 647")) name
1 DTR_2023_ILT_00-Reference Group-DR_CD3 Alexa Fluor 647 (Beads).1237107.fcs
2 DTR_2023_ILT_00-Reference Group-DR_CD3 FITC (Beads).1237108.fcs
3 DTR_2023_ILT_00-Reference Group-DR_Dump_CD14 Pacific Blue (Beads).1237119.fcs
4 DTR_2023_ILT_00-Reference Group-DR_Dump_CD19 Pacific Blue (Beads).1237120.fcs
5 DTR_2023_ILT_00-Reference Group-DR_Va24Ja18 FITC (Beads).1237125.fcs
6 DTR_2023_ILT_00-Reference Group-DR_Va7.2 Alexa Fluor 647 (Beads).1237126.fcs
Fluorophore Antigen Type Detector Negative
1 Alexa Fluor 647 CD3 Beads R2 BeadUnstained
2 FITC CD3 Beads B2 BeadUnstained
3 Pacific Blue Dump_CD14 Beads V3 BeadUnstained
4 Pacific Blue Dump_CD19 Beads V3 BeadUnstained
5 FITC Va24Ja18 Beads B2 BeadUnstained
6 Alexa Fluor 647 Va7.2 Beads R2 BeadUnstained
In this particular case, one Fluorophore (Pacific Blue) is the Dump (Lineage -) fluorophore meant to exclude both B cells (CD19) and Monocytes/Myeloid (CD14) cells. If we check the individual .fcs files, the CD14 staining is the brightest in terms of MFI, so we can likely keep this one for use in the signature matrix.
For the other two Fluorophores (Alexa Fluor 647 and FITC/Alexa Fluor 488), they correspond to the markers used to characterize the Mucosal-associated Invariant T cells (MAITs, Va7.2) and Natural Killer T cells (NKTs, Va24Ja18). Since these can be rare populations, for cell single-color controls, a back-up of the same fluorophore on CD3 instead was also acquired. These in turn were also used for the single-color bead controls to provide equivalents.
Additionally, checking the ImmPort metadata and the paper, the Va24Ja18 antibody vs. hCD1d tetramer were on FITC/Alexa Fluor 488 respectively, so there is an additional single-color present than would be expected.
Ultimately, we can exclude these individual files from use in creating the gates by filtering out their names from the template.
KeptBeads <- BeadMatchTemplate |>
filter(!name %in% "DTR_2023_ILT_00-Reference Group-DR_Dump_CD19 Pacific Blue (Beads).1237120.fcs") |>
filter(!name %in% "DTR_2023_ILT_00-Reference Group-DR_CD3 Alexa Fluor 488 (Beads).1237106.fcs") |>
filter(!name %in% "DTR_2023_ILT_00-Reference Group-DR_CD3 Alexa Fluor 647 (Beads).1237107.fcs") |>
filter(!name %in% "DTR_2023_ILT_00-Reference Group-DR_CD3 FITC (Beads).1237108.fcs") And checking quickly, we can see no duplicate fluorophores are left
KeptBeads |> group_by(Fluorophore) |> mutate(Count=row_number()) |>
ungroup() |> filter(Count > 1) |> pull(Fluorophore) # No duplicatescharacter(0)
KeptBeads name
1 DTR_2023_ILT_00-Reference Group-4BeadsUnstained (Beads).1237095.fcs
2 DTR_2023_ILT_00-Reference Group-DR_CCR4 BUV615 (Beads).1237096.fcs
3 DTR_2023_ILT_00-Reference Group-DR_CCR6 BV786 (Beads).1237097.fcs
4 DTR_2023_ILT_00-Reference Group-DR_CCR7 BV650 (Beads).1237098.fcs
5 DTR_2023_ILT_00-Reference Group-DR_CD107a APC-R700 (Beads).1237099.fcs
6 DTR_2023_ILT_00-Reference Group-DR_CD127 BV421 (Beads).1237100.fcs
7 DTR_2023_ILT_00-Reference Group-DR_CD16 APC (Beads).1237101.fcs
8 DTR_2023_ILT_00-Reference Group-DR_CD161 BV480 (Beads).1237102.fcs
9 DTR_2023_ILT_00-Reference Group-DR_CD25 PE-Cy5 (Beads).1237103.fcs
10 DTR_2023_ILT_00-Reference Group-DR_CD26 PerCP-Cy5.5 (Beads).1237104.fcs
11 DTR_2023_ILT_00-Reference Group-DR_CD27 APC-Fire 750 (Beads).1237105.fcs
12 DTR_2023_ILT_00-Reference Group-DR_CD3 Spark Blue 550 (Beads).1237109.fcs
13 DTR_2023_ILT_00-Reference Group-DR_CD38 APC-Fire 810 (Beads).1237110.fcs
14 DTR_2023_ILT_00-Reference Group-DR_CD4 BUV805 (Beads).1237111.fcs
15 DTR_2023_ILT_00-Reference Group-DR_CD45RA BV510 (Beads).1237112.fcs
16 DTR_2023_ILT_00-Reference Group-DR_CD56 BV605 (Beads).1237113.fcs
17 DTR_2023_ILT_00-Reference Group-DR_CD62L BUV395 (Beads).1237114.fcs
18 DTR_2023_ILT_00-Reference Group-DR_CD69 BUV563 (Beads).1237115.fcs
19 DTR_2023_ILT_00-Reference Group-DR_CD7 BV711 (Beads).1237116.fcs
20 DTR_2023_ILT_00-Reference Group-DR_CD8 BUV496 (Beads).1237117.fcs
21 DTR_2023_ILT_00-Reference Group-DR_CXCR3 BUV737 (Beads).1237118.fcs
22 DTR_2023_ILT_00-Reference Group-DR_Dump_CD14 Pacific Blue (Beads).1237119.fcs
23 DTR_2023_ILT_00-Reference Group-DR_IFNg BV750 (Beads).1237121.fcs
24 DTR_2023_ILT_00-Reference Group-DR_NKG2D PE (Beads).1237122.fcs
25 DTR_2023_ILT_00-Reference Group-DR_PD1 PE-Vio770 (Beads).1237123.fcs
26 DTR_2023_ILT_00-Reference Group-DR_TNFa PE-Dazzle594 (Beads).1237124.fcs
27 DTR_2023_ILT_00-Reference Group-DR_Va24Ja18 FITC (Beads).1237125.fcs
28 DTR_2023_ILT_00-Reference Group-DR_Va7.2 Alexa Fluor 647 (Beads).1237126.fcs
29 DTR_2023_ILT_00-Reference Group-DR_VD2 BUV661 (Beads).1237127.fcs
Fluorophore Antigen Type Detector Negative
1 BeadUnstained <NA> Beads BeadUnstained
2 BUV615 CCR4 Beads UV10 BeadUnstained
3 BV786 CCR6 Beads V15 BeadUnstained
4 BV650 CCR7 Beads V11 BeadUnstained
5 APC-R700 CD107a Beads R4 BeadUnstained
6 BV421 CD127 Beads V1 BeadUnstained
7 APC CD16 Beads R1 BeadUnstained
8 BV480 CD161 Beads V5 BeadUnstained
9 PE-Cy5 CD25 Beads YG5 BeadUnstained
10 PerCP-Cy5.5 CD26 Beads B9 BeadUnstained
11 APC-Fire 750 CD27 Beads R7 BeadUnstained
12 Spark Blue 550 CD3 Beads B3 BeadUnstained
13 APC-Fire 810 CD38 Beads R8 BeadUnstained
14 BUV805 CD4 Beads UV16 BeadUnstained
15 BV510 CD45RA Beads V7 BeadUnstained
16 BV605 CD56 Beads V10 BeadUnstained
17 BUV395 CD62L Beads UV2 BeadUnstained
18 BUV563 CD69 Beads UV9 BeadUnstained
19 BV711 CD7 Beads V13 BeadUnstained
20 BUV496 CD8 Beads UV7 BeadUnstained
21 BUV737 CXCR3 Beads UV14 BeadUnstained
22 Pacific Blue Dump_CD14 Beads V3 BeadUnstained
23 BV750 IFNg Beads V14 BeadUnstained
24 PE NKG2D Beads YG1 BeadUnstained
25 PE-Vio 770 PD1 Beads YG9 BeadUnstained
26 PE-Dazzle 594 TNFa Beads YG3 BeadUnstained
27 FITC Va24Ja18 Beads B2 BeadUnstained
28 Alexa Fluor 647 Va7.2 Beads R2 BeadUnstained
29 BUV661 VD2 Beads UV11 BeadUnstained
We have 29 rows (one including an unstained). The Viability (Zombie NIR) fluorophore was only acquired with cells, so given this datasets context, everything checks out (yes, this is why metadata for an ImmPort repository is important, can you imagine trying to figure this all out without help!?!?!?!)
InitialUnmixSpanGates
As we saw during Week 12, even when working with a single-fluorophore, there are a lot of moving pieces when it comes to extracting out a normalized fluorescent signature from an individual single-color. Combined with the fact that most spectral flow cytometry panels feature more than just one fluorophore, the need to be able to scale up accordingly is important if we have any hope of routinely unmixing in R.
Fortunately, flowGate provides an easy mechanism by which we can create “spanGates”. Now that the new version includes the ability to modify individual gates, this means that once the gates are created, adjusting them after the fact is relatively feasible. The only tricky component remaining was creating peak detector gates based on the correct fluorophore single color control. To faciliate this process, Luciernaga now has an InitialUnmixSpanGates() function, that when provided the GatingSet object and completed template, will iterate through the gate creation process for you. Subsequently, you would only need to adjust the “spanGates” that get created.
Double checking, lets see which gates are currently present on our GatingSet
plot(BeadsSC_GatingSet)
Since we are working with beads, lets grab that final cleanup gate “singlets”. Lets also set the initial gate boundaries (based on percentiles) to 0.75 and 0.80 so that we can make sure to see the gates as we iterate through their creation.
InitialUnmixSpanGates(template=KeptBeads, gs=BeadsSC_GatingSet, subset="singlets", minpercentile=0.75, maxpercentile = 0.99)As the gates get created, the progress bar in turn gets updated

And finally, let’s check what gates are now present
plot(BeadsSC_GatingSet)
CheckUnmixingSpanGates
Alright, the most tedious part of the process (individual peak detector gate creation) is now out of the way. Lets now move on to fixing this initially applied gate placements, because there is no way “minpercentile=0.75” and “maxpercentile = 0.80” are correct across the board.
Behind the Scenes
First off, before running Luciernaga’s CheckUnmixSpanGates() function, lets check some of the basic mechanics that it is iterating through behind the scenes. First, lets start by running gs_apply_gate_check() for the appropiate single-color control specimen, with the corresponding newly created gate.
# pData(BeadsSC_GatingSet) # Sample 16 is the APC-Fire 810 single color unmixing control
gs_apply_gate_check(gs=BeadsSC_GatingSet, gate="APC-Fire 810", sample=16, AdjustAll=FALSE)spanGate should already be selected, to adjust the gate, draw a span gate for the desired region of interest. Then select done.

Sometimes, the plot is off-scale. To rescale, note the approximate X value that you want to increase to, select enable manual coords, enter the value, and then proceed and redraw accordingly (please note some sporadic bugs present)

Lets verify that the gate edit was indeed recorded.
gs_pop_get_gate(BeadsSC_GatingSet, "APC-Fire 810")
As you can see, only the coordinates for our designated specimen were adjusted, with all the other spans remaining at their initial default. If we had wanted wanted to adjust across the board, we would set the AdjustAll argument to FALSE (which we will explore more in-depth during [Week 13] when deriving spillover matrices)
Iterating through the Spans
Now that we have a bit of insight as to the mechanics, lets run CheckUnmixingSpanGates(). It will iterate through the gates designated by the template, launching the Shiny app for each gate sequentially. This wrapper function enables a quicker method by which the gates from InitialUnmixSpanGates() can be adjusted.
CheckUnmixingSpanGates(template=KeptBeads, gs=BeadsSC_GatingSet, AdjustAll=TRUE)



Saving the GatingSet
As we saw, this allows for a rapid initial setup and adjustment of span gates for the peak detector gates matched to their corresponding single-color fluorophore. Now that those gates were created and adjusted, lets save out another intermediate .gs file for subsequent use.
BeadsSCSaveHere <- file.path(OutputLocation, "BeadsSCs_GS_Amplified.gs")
save_gs(BeadsSC_GatingSet, BeadsSCSaveHere)
# BeadsSC_GatingSet <- load_gs(BeadsSCSaveHere )Retrieving Average Signature
At this point, we have created the peak detector gates, and adjusted them on basis of their respective single-color unmixing controls. Now, we just need to retrieve the cells present in those gates, reverse the transformation, and take the average measurement across each detector to get back the combination of the fluorophore + autofluorescence signal.
As we saw during Week 12, individually this can be done relatively simply. But when we start talking specifying out the code for 10, 20, 30, 40, 50 or 60 fluorophores, we end up considering where in life we went wrong and ended up in this line of work. In an attempt to keep us sane just a little longer, we can use the RetrieveAverageSignature() function to manage this step for each of the rows present in our assembled template.
UpdatedTemplate <- RetrieveAverageSignature(template=KeptBeads, gs=BeadsSC_GatingSet, inverse.transform = TRUE)
UpdatedTemplate
As we can see, the retrieved signature values for each fluorophore have been appended on as additional columns. These values still are the combination of both the fluorophore and background, so we need to mediate the autofluorescence subtraction. To do this, we first need to retrieve and add back in the background signal for our negative (BeadsUnstained) which currently is showing up as NA values for its respective row.
BeadUnstained <- GateExprsIterated(gs=BeadsSC_GatingSet, sample=1,
gate="singlets", excludeThese="FSC|SSC|Time|-H")
BeadUnstained[1,1] <- "BeadUnstained"
BeadUnstained
Checking to the UpdatedTemplate, which has the existing columns
colnames(UpdatedTemplate)
So, knowing which row and which column, lets paste it back in
UpdatedTemplate[1,7:ncol(UpdatedTemplate)] <- BeadUnstained[2:ncol(BeadUnstained)]
UpdatedTemplate[1,2] <- "BeadUnstained"
UpdatedTemplate
Background Subtraction
With the template now containing the detector values for both our fluorophores, as well as the unstained, we can now proceed to subtract out the background. This can be done using the BackgroundSubtraction() wrapper from Lucierngaga, which mediates the subtraction by matching the name found in the Negative column to the matching unstained name found in the Fluorophore column. We also have the option to go ahead and normalize (i.e. scale the signal from 0 to 1) and whether to keep the unstained values or not (in this case since we are working with beads, lets keep it FALSE (since we won’t be calculating bead autofluorescence from our cells at the time of unmixing))
BeadFluorophoreSignatures <- BackgroundSubtraction(data=UpdatedTemplate, Normalize=TRUE, keepNegatives=FALSE)
Before we go anywhere else, lets save this signature matrix out as a .csv for safe keeping/comparison.
StoreHere <- file.path(OutputLocation, "BeadInitialSignatureMatrix.csv")
write.csv(BeadFluorophoreSignatures, StoreHere, row.names=FALSE)Which can then subsequently be read back in
StoreHere <- file.path(OutputLocation, "BeadInitialSignatureMatrix.csv")
BeadFluorophoreSignatures <- read.csv(StoreHere, check.names=FALSE)Alright, lets check and see how atrocious our initial span gates really were
Plot <- VisualizeSignatures(columnname="Fluorophore", data=BeadFluorophoreSignatures,
Normalize=FALSE, characterColumns="Fluorophore")
plotly::ggplotly(Plot)And those look relatively reasonable at first glance, lets power on and tackle the cell single-color unmixing controls next.
Cell Single-Colors
Having made our way through the bead single color unmixing controls, lets repeat the process with the cell single colors. To do this, let load them in from their intermediate .gs
CellSCsStoredHere <- file.path(OutputLocation, "CellsSCs_GS.gs")
CellsSC_GatingSet <- load_gs(CellSCsStoredHere )Updating Metadata
Lets quickly check the existing metadata, and see if the names have changed compared to the bead unmixing controls
pData(CellsSC_GatingSet)
Overall, the name syntax for these files is similar to what we saw for the bead single-color unmixing controls. We will likely be able to use the same extraction code we used for the beads, with only a few modifications needed.
cell_pd <- pData(CellsSC_GatingSet)
Updated_cell_pd <- cell_pd |> mutate(
raw = str_extract(name, "(?<=DR_)[^(]+(?=\\()"),
Antigen = raw |>
str_trim() |>
str_extract("^\\S+"),
Fluorophore = raw |>
str_trim() |>
str_remove("^\\S+\\s+"),
Type = name |>
str_extract("(?<=\\()([^)]+)(?=\\))"),
raw = NULL
)
Updated_cell_pd <- Updated_cell_pd |> relocate(Fluorophore, Antigen, Type, .after="name")
Updated_cell_pd
Since no unstained samples are present in this GatingSet, the only adjustment we need to carry out is changing PE-Vio770 and PE-Dazzle594 to include the space in the name to allow the DetectorGuess() function to correctly assign the peak detector later on.
Updated_cell_pd <- Updated_cell_pd |>
mutate(Fluorophore = case_when(
Fluorophore == "PE-Vio770" ~ "PE-Vio 770",
Fluorophore == "PE-Dazzle594" ~ "PE-Dazzle 594",
TRUE ~ Fluorophore
))
pData(CellsSC_GatingSet) <- Updated_cell_pd
pData(CellsSC_GatingSet)
Template Set Up
At this point, we can proceed to generate the template that we will need for gating. In this case, lets start off by specifying everything as using a “CellUnstained” negative, and then iterate through the Fluorophore column with DetectorGuess() to retrieve the peak detectors.
Template <- pData(CellsSC_GatingSet)
Template <- Template |> mutate(Detector="", Negative="CellUnstained")
Template <- Template |> mutate(
Detector = map_chr(Fluorophore, \(f) DetectorGuess(x = f, NumberDetectors = 64))
)
Template
In this case, however, we have a bit more variety in terms of the negative background being used. Squinting at the lab notebook scan

Looks like the CD8 (BUV496), CD45RA (BV510), CD56 (BV605), PD1 (PE-Vio 770), CD16 (APC), CD38 (APC-Fire 810), and 1 of the Viability (Zombie NIR) were prepared using extra cord blood mononuclear cells. Similarly, the CD69 (BUV563), TNFa (PE-Dazzle 594), IFNg (BV750), CD107a (APC-R700), CD25 (PE-Cy5) and the other Viability (Zombie NIR) were prepared using PMA-ionomycin stimulated PBMC. All other controls would have been unstimulated PBMC.
And as was previously mentioned, we will need to match the cell gating background for the Pacific Blue CD14 (Monocyte gated) and both Zombie NIR controls (dead cell gated) when the time comes.
To cut down on the modifications needed in the future, lets modify the template to account for these different negative backgrounds using the dplyr packages case_when() and mutate() functions.
Updated_cell_pd <- Template |>
mutate(Negative = case_when(
# Different Gate Placements
Antigen == "Dump_CD14" ~ "PBMC_Monocyte_Unstained",
Antigen == "Viability" ~ "CBMC_Dead_Unstained",
Antigen == "Viability_PMA" ~ "PBMC_Dead_Unstained",
# CBMC
Antigen == "CD8" ~ "CBMC_Unstained",
Antigen == "CD45RA" ~ "CBMC_Unstained",
Antigen == "CD56" ~ "CBMC_Unstained",
Antigen == "PD1" ~ "CBMC_Unstained",
Antigen == "CD16" ~ "CBMC_Unstained",
Antigen == "CD38" ~ "CBMC_Unstained",
# PMA-stimulated PBMC
Antigen == "CD69" ~ "PBMC_Activated_Unstained",
Antigen == "TNFa" ~ "PBMC_Activated_Unstained",
Antigen == "IFNg" ~ "PBMC_Activated_Unstained",
Antigen == "CD107a" ~ "PBMC_Activated_Unstained",
Antigen == "CD25" ~ "PBMC_Activated_Unstained",
# All Others
TRUE ~ "PBMC_Unstained"
))
Updated_cell_pd <- Updated_cell_pd |> relocate(Fluorophore, Antigen, Type, Detector, Negative, .after="name")
With these adjustments made, we can then proceed to incorporate it back into the GatingSet metadata via the assignment arrow and pData() function
pData(CellsSC_GatingSet) <- Updated_cell_pdAnd for insurance purposes, lets go ahead and store it as a .csv file as well.
Template <- pData(CellsSC_GatingSet)
StoreHere <- file.path(OutputLocation, "CellMatch.csv")
write.csv(Template, StoreHere, row.names=FALSE)StoreHere <- file.path(OutputLocation, "UpdatedCellMatch.csv")
CellMatchTemplate <- read.csv(StoreHere, check.names=FALSE)Removing Duplicates
Once again, lets check for duplicates
CellMatchTemplate |> group_by(Fluorophore) |> mutate(Count=row_number()) |>
ungroup() |> filter(Count > 1) |> pull(Fluorophore) # Spot duplicated fluorophore references[1] "Pacific Blue" "Zombie NIR"
Both Pacific Blue and Zombie NIR come up as we had encountered with the beads. Checking the files, we opt to keep the duplicate that contains the brighter staining events. This results in keeeping Pacific Blue Dump_CD14 which is considerably brighter compared to the CD19, and the CBMC based Zombie NIR viability control (which is brighter than the PMA-ionomycin stimulated PBMC sample). We can also also exclude the redundant Alexa Fluor 488 control.
KeptCells <- CellMatchTemplate |>
filter(!name %in% "DTR_2023_ILT_01-Reference Group-DR_Dump_CD19 Pacific Blue (Cells).1235702.fcs") |>
filter(!name %in% "DTR_2023_ILT_01-Reference Group-DR_Viability_PMA Zombie NIR (Cells).1235709.fcs") |>
filter(!name %in% "DTR_2023_ILT_01-Reference Group-DR_CD3 Alexa Fluor 488 (Cells).1235688.fcs")
KeptCells name
1 DTR_2023_ILT_01-Reference Group-DR_CCR4 BUV615 (Cells).1235678.fcs
2 DTR_2023_ILT_01-Reference Group-DR_CCR6 BV786 (Cells).1235679.fcs
3 DTR_2023_ILT_01-Reference Group-DR_CCR7 BV650 (Cells).1235680.fcs
4 DTR_2023_ILT_01-Reference Group-DR_CD107a APC-R700 (Cells).1235681.fcs
5 DTR_2023_ILT_01-Reference Group-DR_CD127 BV421 (Cells).1235682.fcs
6 DTR_2023_ILT_01-Reference Group-DR_CD16 APC (Cells).1235683.fcs
7 DTR_2023_ILT_01-Reference Group-DR_CD161 BV480 (Cells).1235684.fcs
8 DTR_2023_ILT_01-Reference Group-DR_CD25 PE-Cy5 (Cells).1235685.fcs
9 DTR_2023_ILT_01-Reference Group-DR_CD26 PerCP-Cy5.5 (Cells).1235686.fcs
10 DTR_2023_ILT_01-Reference Group-DR_CD27 APC-Fire 750 (Cells).1235687.fcs
11 DTR_2023_ILT_01-Reference Group-DR_CD3 Alexa Fluor 647 (Cells).1235689.fcs
12 DTR_2023_ILT_01-Reference Group-DR_CD3 FITC (Cells).1235690.fcs
13 DTR_2023_ILT_01-Reference Group-DR_CD3 Spark Blue 550 (Cells).1235691.fcs
14 DTR_2023_ILT_01-Reference Group-DR_CD38 APC-Fire 810 (Cells).1235692.fcs
15 DTR_2023_ILT_01-Reference Group-DR_CD4 BUV805 (Cells).1235693.fcs
16 DTR_2023_ILT_01-Reference Group-DR_CD45RA BV510 (Cells).1235694.fcs
17 DTR_2023_ILT_01-Reference Group-DR_CD56 BV605 (Cells).1235695.fcs
18 DTR_2023_ILT_01-Reference Group-DR_CD62L BUV395 (Cells).1235696.fcs
19 DTR_2023_ILT_01-Reference Group-DR_CD69 BUV563 (Cells).1235697.fcs
20 DTR_2023_ILT_01-Reference Group-DR_CD7 BV711 (Cells).1235698.fcs
21 DTR_2023_ILT_01-Reference Group-DR_CD8 BUV496 (Cells).1235699.fcs
22 DTR_2023_ILT_01-Reference Group-DR_CXCR3 BUV737 (Cells).1235700.fcs
23 DTR_2023_ILT_01-Reference Group-DR_Dump_CD14 Pacific Blue (Cells).1235701.fcs
24 DTR_2023_ILT_01-Reference Group-DR_IFNg BV750 (Cells).1235703.fcs
25 DTR_2023_ILT_01-Reference Group-DR_NKG2D PE (Cells).1235704.fcs
26 DTR_2023_ILT_01-Reference Group-DR_PD1 PE-Vio770 (Cells).1235705.fcs
27 DTR_2023_ILT_01-Reference Group-DR_TNFa PE-Dazzle594 (Cells).1235706.fcs
28 DTR_2023_ILT_01-Reference Group-DR_VD2 BUV661 (Cells).1235707.fcs
29 DTR_2023_ILT_01-Reference Group-DR_Viability Zombie NIR (Cells).1235708.fcs
Fluorophore Antigen Type Detector Negative
1 BUV615 CCR4 Cells UV10 PBMC_Unstained
2 BV786 CCR6 Cells V15 PBMC_Unstained
3 BV650 CCR7 Cells V11 PBMC_Unstained
4 APC-R700 CD107a Cells R4 PBMC_Activated_Unstained
5 BV421 CD127 Cells V1 PBMC_Unstained
6 APC CD16 Cells R1 CBMC_Unstained
7 BV480 CD161 Cells V5 PBMC_Unstained
8 PE-Cy5 CD25 Cells YG5 PBMC_Activated_Unstained
9 PerCP-Cy5.5 CD26 Cells B9 PBMC_Unstained
10 APC-Fire 750 CD27 Cells R7 PBMC_Unstained
11 Alexa Fluor 647 CD3 Cells R2 PBMC_Unstained
12 FITC CD3 Cells B2 PBMC_Unstained
13 Spark Blue 550 CD3 Cells B3 PBMC_Unstained
14 APC-Fire 810 CD38 Cells R8 CBMC_Unstained
15 BUV805 CD4 Cells UV16 PBMC_Unstained
16 BV510 CD45RA Cells V7 CBMC_Unstained
17 BV605 CD56 Cells V10 CBMC_Unstained
18 BUV395 CD62L Cells UV2 PBMC_Unstained
19 BUV563 CD69 Cells UV9 PBMC_Activated_Unstained
20 BV711 CD7 Cells V13 PBMC_Unstained
21 BUV496 CD8 Cells UV7 CBMC_Unstained
22 BUV737 CXCR3 Cells UV14 PBMC_Unstained
23 Pacific Blue Dump_CD14 Cells V3 PBMC_Monocyte_Unstained
24 BV750 IFNg Cells V14 PBMC_Activated_Unstained
25 PE NKG2D Cells YG1 PBMC_Unstained
26 PE-Vio 770 PD1 Cells YG9 CBMC_Unstained
27 PE-Dazzle 594 TNFa Cells YG3 PBMC_Activated_Unstained
28 BUV661 VD2 Cells UV11 PBMC_Unstained
29 Zombie NIR Viability Cells R6 CBMC_Dead_Unstained
Considering this panel has 29-fluorophores, and we have 29-rows, looks like we are good. We will need to add in the unstaineds separately later on.
Initial UnmixSpanGates
Before creating the peak detector gates, lets double check the existing gates for the CellsSC_GatingSet
plot(CellsSC_GatingSet)
At this point, we can proceed to create the initial peak detector gates via InitialUnmixSpanGagtes(), placing them under the existing “scatter” cleanup gate, monitoring the progress bar as we wait.
InitialUnmixSpanGates(template=KeptCells, gs=CellsSC_GatingSet, subset="scatter", minpercentile=0.75, maxpercentile = 0.99)
We can then quickly verify that the initial peak detector gates have been created.
plot(CellsSC_GatingSet)
Remaining Gate Adjustment
At the moment, all the initial peak detector gates have been created based of the parent “scatter” gate. However, for Zombie NIR Viability we are set to use a CBMC dead gate, and for Pacific Blue CD14 we will be using the monocytes gate. Before proceeding, we should double check that the previous gate shifts implemented are still in place.
pData(CellsSC_GatingSet) |> mutate(WhereAt = dplyr::row_number())
# PacificBlue Dump_CD14 is #24 based off above pData
gs_apply_gate_check(gs=CellsSC_GatingSet, gate="scatter", sample=24, AdjustAll=FALSE)
# Zombie NIR CBMC is #31 based off above pData
gs_apply_gate_check(gs=CellsSC_GatingSet, gate="scatter", sample=31, AdjustAll=FALSE)
Checking UnmixingSpanGates
With the peak detector gates now created, we can run CheckUnmixingSpanGates() to iterate through them and allow us to adjust any of the spans that are off via the flowGate ShinyApp.
CheckUnmixingSpanGates(template=KeptCells, gs=CellsSC_GatingSet, AdjustAll=TRUE)If the gate is good as is, just click done to move to the next specimen

Alternatively, draw a span, and then select done

And repeat for the rest of the single-colors, attempting as much as possible to stick to the best practices for unmixing controls.

Saving the GatingSet
Having created the peak detector gates, and ensured they are not horridly off for their respective single-color control specimens, go ahead and save this state as its own intermediate .gs object to allow for quick reloading for any subsequent steps.
SaveUpdatedCellSCHere <- file.path(OutputLocation, "CellsSC_GS_Amplified.gs")
save_gs(CellsSC_GatingSet, SaveUpdatedCellSCHere)
# CellsSC_GatingSet <- load_gs(SaveUpdatedCellSCHere)Retrieving Average Signature
With our span gates checked, we can proceed to expand the template, iterating through each row and retrieving the median signature for each detector using the RetrieveAverageSignature() wrapper function.
#KeptCellsBackup <- KeptCells
#KeptCells <- KeptCellsBackup[10,]
UpdatedTemplate <- RetrieveAverageSignature(template=KeptCells, gs=CellsSC_GatingSet, inverse.transform = TRUE)
Assembling Negative Backgrounds
Unlike the Bead GatingSet that contained its own unstained, we will need to select several unstained samples to use as background subtraction for our single-colors.
KeptCells |> pull(Negative) |> unique()[1] "PBMC_Unstained" "PBMC_Activated_Unstained"
[3] "CBMC_Unstained" "PBMC_Monocyte_Unstained"
[5] "CBMC_Dead_Unstained"
UnstainedGS
Lets retrieve the CellUnstained_GS from where we stored it as a .gs folder in the last walkthrough.
UnstainedIntermediateStoredHere <- file.path(OutputLocation, "CellsUnstained_GS.gs")
Unstained_GatingSet <- load_gs(UnstainedIntermediateStoredHere)pData(Unstained_GatingSet)
Drawing Final Gates
After squinting away through our lab notebook scan, it looks like the PBMC controls were sourced from the ND006 specimens. CBMC originated from INF052 specimen. We still need to draw a monocyte and dead cell gates, since the current gating scheme appears as follows
plot(Unstained_GatingSet)
Lets tackle the addition of those new gates first, basing them off the parent gate (nonRBCs)
pd <- pData(Unstained_GatingSet)
pd |> mutate(WhichRow = dplyr::row_number())
First lets create a Monocyte gate based off the CBMC specimen.
flowGate::gs_gate_interactive(gs=Unstained_GatingSet, filterId="Monocytes", sample=1,
dims=list("FSC-A", "SSC-A"), subset="nonRBCs") # INF052 Control
And lets check it for the adult PBMC specimen without stimulation.
gs_apply_gate_check(gs=Unstained_GatingSet, gate="Monocytes", sample=9, AdjustAll=FALSE)
We can then draw gates for dead/dying CBMC
flowGate::gs_gate_interactive(gs=Unstained_GatingSet, filterId="CordDead", sample=1,
dims=list("FSC-A", "SSC-A"), subset="nonRBCs") # INF052 Control
As well as dead/dying PBMC following PMA-ionomycin activation
flowGate::gs_gate_interactive(gs=Unstained_GatingSet, filterId="PBMCDead", sample=10,
dims=list("FSC-A", "SSC-A"), subset="nonRBCs", regate=TRUE) # NDOO6 PMA
And if we now check the created gates
plot(Unstained_GatingSet)
Saving the GatingSet
And now that these final gates are drawn and adjusted, lets go ahead and similarly save this GatingSet out as a new intermediate .gs folder for future use.
CellUnstainedStoredHere <- file.path(OutputLocation, "CellsUnstained_GS_Amplified.gs")
save_gs(Unstained_GatingSet, CellUnstainedStoredHere)
# Unstained_GatingSet <- load_gs(CellUnstainedStoredHere)Extracting Autofluorescence Signature
Now that we have the unstained specimens loaded, and last of the positional gates created, we are ready to extract out the signatures from each, and tidy the data to match the template format being used by our single colors. Lets start by first extracting the median fluorescent signature values for each.
# gs_apply_gate_check(gs=Unstained_GatingSet, gate="scatter", sample=9, AdjustAll=FALSE)
# plot(Unstained_GatingSet)
PBMC_Unstained <- GateExprsIterated(gs=Unstained_GatingSet, sample=9,
gate="scatter", excludeThese="FSC|SSC|Time|-H", inverse.transform=TRUE)
PBMC_Unstained[1,1] <- "PBMC_Unstained"# pData(CellsSC_GatingSet) |> mutate(WhichRow=row_number())
# gs_apply_gate_check(gs=CellsSC_GatingSet, gate="Pacific Blue", sample=24, AdjustAll=FALSE)
# gs_apply_gate_check(gs=Unstained_GatingSet, gate="Monocytes", sample=9, AdjustAll=FALSE)
# plot(Unstained_GatingSet)
PBMC_Monocyte_Unstained <- GateExprsIterated(gs=Unstained_GatingSet, sample=9,
gate="Monocytes", excludeThese="FSC|SSC|Time|-H", inverse.transform=TRUE)
PBMC_Monocyte_Unstained[1,1] <- "PBMC_Monocyte_Unstained"# gs_apply_gate_check(gs=Unstained_GatingSet, gate="scatter", sample=10, AdjustAll=FALSE)
# plot(Unstained_GatingSet)
PBMC_Activated_Unstained <- GateExprsIterated(gs=Unstained_GatingSet, sample=10,
gate="scatter", excludeThese="FSC|SSC|Time|-H", inverse.transform=TRUE)
PBMC_Activated_Unstained[1,1] <- "PBMC_Activated_Unstained"# gs_apply_gate_check(gs=Unstained_GatingSet, gate="PBMCDead", sample=10, AdjustAll=FALSE)
# plot(Unstained_GatingSet)
PBMC_Dead_Unstained <- GateExprsIterated(gs=Unstained_GatingSet, sample=10,
gate="PBMCDead", excludeThese="FSC|SSC|Time|-H", inverse.transform=TRUE)
PBMC_Dead_Unstained[1,1] <- "PBMC_Dead_Unstained"# gs_apply_gate_check(gs=Unstained_GatingSet, gate="scatter", sample=1, AdjustAll=FALSE)
# plot(Unstained_GatingSet)
CBMC_Unstained <- GateExprsIterated(gs=Unstained_GatingSet, sample=1,
gate="scatter", excludeThese="FSC|SSC|Time|-H", inverse.transform=TRUE)
CBMC_Unstained[1,1] <- "CBMC_Unstained"# gs_apply_gate_check(gs=Unstained_GatingSet, gate="CordDead", sample=1, AdjustAll=FALSE)
# plot(Unstained_GatingSet)
CBMC_Dead_Unstained <- GateExprsIterated(gs=Unstained_GatingSet, sample=1,
gate="CordDead", excludeThese="FSC|SSC|Time|-H", inverse.transform=TRUE)
CBMC_Dead_Unstained[1,1] <- "CBMC_Dead_Unstained"And lets go ahead and combine these together into a data.frame to simplify the data tidying steps downstream.
UnstainedData <- bind_rows(PBMC_Unstained, PBMC_Monocyte_Unstained, PBMC_Activated_Unstained,
PBMC_Dead_Unstained, CBMC_Unstained, CBMC_Dead_Unstained)
UnstainedData
Visualizing Autofluorescencce signatures
And for curiosity while we are here, lets visualize the signatures, both raw and normalized
Plots <- Luciernaga::VisualizeSignatures(data=UnstainedData, columnname="Fluorophore", Normalize = FALSE)
plotly::ggplotly(Plots)Plots <- Luciernaga::VisualizeSignatures(data=UnstainedData, columnname="Fluorophore", Normalize = TRUE)Normalizing Data for Signature Comparison
plotly::ggplotly(Plots)So overall, some differences in brightness, but mostly appear to be similar in terms of normalized signature.
Data Tidying
Now that we have the signature values for the detector columns and identifying “Fluorophore” name, we need to do some data tidying to fit these values from our unstained controls into the format needed for the Background subtraction template.
Lets start by slicing the first row from our existing template.
CellStandin <- UpdatedTemplate |> slice(1)
Lets then duplicate the rows to match the number of rows in UnstainedData
CellStandin <- CellStandin[rep(1, nrow(UnstainedData)), ]
rownames(CellStandin) <- NULLLets now follow up by blanking some of the metadata columns
# Note: PLEASE VERIFY CORRECT COLUMNS WHEN HARD-CODING LIKE THIS
# colnames(CellStandin)
CellStandin[,1] <- "" # name
CellStandin[,2] <- "" # Fluorophore
CellStandin[,3] <- "" # Antigen
CellStandin[,4] <- "Cells" # Type
CellStandin[,5] <- "" # Detector
CellStandin[,6] <- "" # Negative
CellStandin
Lets cut and paste in the detector values over from UnstainedData into CellStandin, and then place the names into a few of the metadata column locations.
CellStandin[,7:ncol(CellStandin)] <- UnstainedData[2:ncol(UnstainedData)]
CellStandin[,c(1,2,6)] <- UnstainedData[1]
CellStandin
At this point, we can combine in with the UpdatedTemplate to form the completed template we need for background subtraction.
UpdatedTemplate <- bind_rows(CellStandin, UpdatedTemplate)
UpdatedTemplate
Background Subtraction
With the template now assembled, lets proceed to subtract out the correct background from each of the single-color controls
CellFluorophoreSignatures <- BackgroundSubtraction(data=UpdatedTemplate, Normalize=TRUE, keepNegatives=TRUE)
And once again, before we loose our hard work, lets go ahead and save these initial signatures to a .csv file.
StoreHere <- file.path(OutputLocation, "CellInitialSignatureMatrix.csv")
write.csv(CellFluorophoreSignatures, StoreHere, row.names=FALSE)Which can then subsequently be read back in
StoreHere <- file.path(OutputLocation, "CellInitialSignatureMatrix.csv")
CellFluorophoreSignatures <- read.csv(StoreHere, check.names=FALSE)And lets visualize the outputs
Plot <- VisualizeSignatures(columnname="Fluorophore", data=CellFluorophoreSignatures,
Normalize=FALSE, characterColumns="Fluorophore")
plotly::ggplotly(Plot)Take Away
In this walk-through, we explored how to orchestrate the creation and adjustment of peak detector gates for our single-color controls. Once these gates were setup, for every fluorophore in our panel, we extracted the median values for the cells contained within those gates, subtracted the background and then normalized the values. These signature matrices where then stored as .csv files so that we can compare and contrast the signatures during another walkthrough. Whether the extracted averaged fluorescent signatures are indeed correct and will function properly in unmixing (or if we need to go back and redraw our spanGates), is something we will explore again in the next walk-through.


