remotes::install_github("DrCytometer/AutoSpectral")
remotes::install_github("DrCytometer/AutoSpectralRcpp")
pak::pkg_install("DrCytometer/AutoSpectral")
pak::pkg_install("DrCytometer/AutoSpectralRcpp")Package Walkthrough: AutoSpectral

For the YouTube livestream schedule, see here
For screen-shot slides, click here

Background
There are different ways to unmix spectral flow cytometry data. Autospectral is one of them, with the methodology behind the approach explained in this 2025 pre-print by Oliver Burton et al.
Logistics
Method
Walk-through
Installation
I recommend starting off by installing both the AutoSpectral and its associated AutoSpectalRcpp package (which allows for accelerated unmixing via C++ code) at the same time. Since both are available via GitHub, they can be installed using either the remotes or pak packages.
Please note, if you are on a MacOS or Linux operating system, to install AutoSpectralRcpp you will likely need to configure some system settings first to allow you to make full use of OpenMP (as mentioned by Oliver in the AutoSpectralRcpp README).
On Debian Linux (Trixie), the steps to are summarized below
First, create and open a personal ~/.R/Makevars file via your terminal
# bash/shell code
mkdir .p ~/.R
nano ~/.R/MakevarsEdit the file by entering the file, before saving the changes and exiting.
# bash/shell code
CXX_STD = CXX17
PKG_CXXFLAGS = -fopenmp
PKG_LIBS = -fopenmp -llapack -lblasOnce the Makevars file has been updated, proceed with installing AutoSpectralRcpp.
Dataset
For this walkthrough, the spectral flow cytometry .fcs files being used were generated in 2025 as part of the yearly 5-day training workshop here at UMGCCC FCSR. The panel consisted of 5 markers (CD45, CD3, CD4, CD8 and Viability). This panel was used to stain mouse splenocytes, which were then acquired on a 5-laser Cytek Aurora. These .fcs files were substantially downsampled in order to pass the GitHub size limits for sharing as part of this example.
Set Up
Similar to it’s predecessor autospill, some of the AutoSpectral functions are a bit opinionated on where folders and files should be in relation to the working directory. Rather than encountering a bug that might be difficult to troubleshoot, I recommend just creating a new project folder in which you will primarily be carrying out the unmixing. In this scenario, all created folders and files end up at the top-level of your projects working directory.
To get started, first attach AutoSpectral to your local environment the required R packages via the library() call
library(AutoSpectral)
library(AutoSpectralRcpp)
Attaching package: 'AutoSpectralRcpp'
The following object is masked from 'package:AutoSpectral':
sanitize.optimization.inputs
library(dplyr)
Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
Locate .fcs files
Next off, specify the storage location of the .fcs files. In the case of todays example data, the unmixing controls and full-stained samples are present in their own respective subfolders within “data”. Additionally, provide a file.path to a folder where you want the unmixed .fcs files to be saved to.
# StorageLocation <- file.path("course", "community", "AutoSpectral", "data")
StorageLocation <- file.path("data") # For Quarto Render
# OutputLocation <- file.path("course", "community", "AutoSpectral", "outputs")
OutputLocation <- file.path("outputs") # For Quarto Renderasp
AutoSpectral, like its predecessor package autospill, primarily utilizes an “asp” list object for use in specifying default arguments for subsequent unmixing.
Main things to specify is the spectral cytometer used (currently consisting of “aurora”, “auroraNL”, “id7000”, “a8”, “s8”, “a5se”, “opteon”, “mosaic” and “xenith”) and whether you want the plots to be saved as figures within their respective folders.
Since this dataset was from a Cytek Aurora, and generally the more visual data we have to ensure the gates were set properly the better, we will go with
asp <- get.autospectral.param(
cytometer = "aurora",
figures = TRUE
)
Control File
Creating the Control File
Next up, we will assemble the Control File, which is the .csv file that allows us to dictate what background is substracted from each of the singe-color controls.
To get started, we need to provide the file.path to the directory that contains our unmixing controls (primarily single-color + 1 unstained).
control.dir <- file.path(StorageLocation, "unmixing_controls")Alongside our already generated “asp” list, we can pass these as arguments to the create.control.file() to create the “fcs_control_file.csv”
create.control.file(control.dir, asp)As the code runs, AutoSpectral checks the fluorophore names against a reference database. If it matches a known fluorophore name, this will be used to prefill in some of the columns in the control file. If a fluorophore is not present in the database, a warning message will be outputted to the console window. In case of a new fluorophore that is not present in the database, you would then need to manually fill the corresponding information.
In our case, it didn’t find a matching name for our unstained, which is expected, as it is not a fluorophore to begin with.

The resulting control file ends up being written directly to the top-level of our working directory (similar to how it was done with autospill).

Since we created a new project folder for unmixing, this is expected and the files should remain at the top-level of the working directory.
In the case of the wesite example, I copied these outputs over to the “outputs” folder for documentation and to allow the website to build correctly
Lets quickly visualize the contents of the Control File
CSVFile <- list.files(pattern="fcs_control_file.csv",
full.names=TRUE) # For YouControlFile filename fluorophore marker
1 PD_AD_CD8 BV510 (Cells)_scatter.fcs BV510 CD8
2 PD_AD_CD4 BV605 (Cells)_scatter.fcs BV605 CD4
3 PD_AD_CD3 Spark Blue 550 (Cells)_scatter.fcs Spark Blue 550 CD3
4 PD_AD_CD45 Alexa Fluor 700 (Cells)_scatter.fcs Alexa Fluor 700 CD45
5 PD_AD_Viability 7-AAD (Cells)_scatter.fcs 7-AAD Viability
6 Unstained (Cells)_scatter.fcs AF
channel control.type universal.negative large.gate gate.name
1 V7-A cells NA FALSE smallGate_1
2 V10-A cells NA FALSE smallGate_1
3 B3-A cells NA FALSE smallGate_1
4 R4-A cells NA FALSE smallGate_1
5 YG-4 cells NA FALSE viabilityGate_2
6 cells NA FALSE smallGate_1
gate.define is.viability
1 TRUE FALSE
2 TRUE FALSE
3 TRUE FALSE
4 TRUE FALSE
5 TRUE TRUE
6 TRUE FALSE
The main structural elements to the control file are the following columns
- filename: The actual .fcs filename
- fluorophore: The name of the fluorophore (or AF in case of the unstained)
- marker: The name of the marker (or left empty in the case of the unstained)
- channel: The peak detector channel of the fluorophore (left empty for the unstained). In case the fluorophore is not present in the reference library, you will need to specify this manually
- control.type: Whether the unmixing control is cells or beads (used for matching positive and negatives for background subtraction)
- universal.negative: By default empty, if you want to use an universal negative, copy the filename of the corresponding unstained into this spot on a per fluorophore basis
- large.gate: Logical value, by default FALSE, set to TRUE to expand the gating area (more details later)
- gate.name: Gate name for use in signature retrievel (more details later)
- gate.define: Logical, permits you to set the general gate region as defined by gate.name
- is.viability: Whether your control is a viability dye. In this case, correctly identified 7-AAD from reference database so is already set to TRUE.
Editing the Control File
In this case, the reference databse correctly set most things at setup, so only a handful of manual interventions are needed. Primarily, we will be using the universal.negative option, so we will need to copy the filename of our Unstained (“Unstained (Cells)_scatter.fcs”) to fill the empty slots in the universal.negative column. If you were editing the data.frame in R, you could do this for the entire column as follows
ControlFile$universal.negative <- "Unstained (Cells)_scatter.fcs"At the time of this walk-through, AutoSpectral failed to parse the channel information correctly for 7-AAD (showing up only as YG4 instead of YG4-A), so we will go ahead and also correct this

ControlFile <- ControlFile |>
mutate(channel = case_when(
fluorophore == "7-AAD" ~ "YG4-A",
TRUE ~ channel
))ControlFile filename fluorophore marker
1 PD_AD_CD8 BV510 (Cells)_scatter.fcs BV510 CD8
2 PD_AD_CD4 BV605 (Cells)_scatter.fcs BV605 CD4
3 PD_AD_CD3 Spark Blue 550 (Cells)_scatter.fcs Spark Blue 550 CD3
4 PD_AD_CD45 Alexa Fluor 700 (Cells)_scatter.fcs Alexa Fluor 700 CD45
5 PD_AD_Viability 7-AAD (Cells)_scatter.fcs 7-AAD Viability
6 Unstained (Cells)_scatter.fcs AF
channel control.type universal.negative large.gate gate.name
1 V7-A cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
2 V10-A cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
3 B3-A cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
4 R4-A cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
5 YG4-A cells Unstained (Cells)_scatter.fcs FALSE viabilityGate_2
6 cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
gate.define is.viability
1 TRUE FALSE
2 TRUE FALSE
3 TRUE FALSE
4 TRUE FALSE
5 TRUE TRUE
6 TRUE FALSE
For both the Unstained and CD45, I will also set the gate.define argument to FALSE for now.
ControlFile <- ControlFile |>
mutate(gate.define = case_when(
marker == "CD45" ~ FALSE,
fluorophore == "AF" ~ FALSE,
TRUE ~ gate.define
))ControlFile filename fluorophore marker
1 PD_AD_CD8 BV510 (Cells)_scatter.fcs BV510 CD8
2 PD_AD_CD4 BV605 (Cells)_scatter.fcs BV605 CD4
3 PD_AD_CD3 Spark Blue 550 (Cells)_scatter.fcs Spark Blue 550 CD3
4 PD_AD_CD45 Alexa Fluor 700 (Cells)_scatter.fcs Alexa Fluor 700 CD45
5 PD_AD_Viability 7-AAD (Cells)_scatter.fcs 7-AAD Viability
6 Unstained (Cells)_scatter.fcs AF
channel control.type universal.negative large.gate gate.name
1 V7-A cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
2 V10-A cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
3 B3-A cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
4 R4-A cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
5 YG4-A cells Unstained (Cells)_scatter.fcs FALSE viabilityGate_2
6 cells Unstained (Cells)_scatter.fcs FALSE smallGate_1
gate.define is.viability
1 TRUE FALSE
2 TRUE FALSE
3 TRUE FALSE
4 FALSE FALSE
5 TRUE TRUE
6 FALSE FALSE
Lastly, I wish to rename the gate.names, preferring to use “lymphocytes” and “dead” as terms vs. “smallGate”, “viabilityGate” respectively. Since all these markers are primarily lymphocyte markers, I will overwrite everthing as “lymphocytes”, before using dplyr’s case_when() function to selectively set 7-AAD’s value for gate.name to “dead”
ControlFile$gate.name <- "lymphocytes"
ControlFile <- ControlFile |>
mutate(gate.name = case_when(
fluorophore == "7-AAD" ~ "dead",
TRUE ~ gate.name
))ControlFile filename fluorophore marker
1 PD_AD_CD8 BV510 (Cells)_scatter.fcs BV510 CD8
2 PD_AD_CD4 BV605 (Cells)_scatter.fcs BV605 CD4
3 PD_AD_CD3 Spark Blue 550 (Cells)_scatter.fcs Spark Blue 550 CD3
4 PD_AD_CD45 Alexa Fluor 700 (Cells)_scatter.fcs Alexa Fluor 700 CD45
5 PD_AD_Viability 7-AAD (Cells)_scatter.fcs 7-AAD Viability
6 Unstained (Cells)_scatter.fcs AF
channel control.type universal.negative large.gate gate.name
1 V7-A cells Unstained (Cells)_scatter.fcs FALSE lymphocytes
2 V10-A cells Unstained (Cells)_scatter.fcs FALSE lymphocytes
3 B3-A cells Unstained (Cells)_scatter.fcs FALSE lymphocytes
4 R4-A cells Unstained (Cells)_scatter.fcs FALSE lymphocytes
5 YG4-A cells Unstained (Cells)_scatter.fcs FALSE dead
6 cells Unstained (Cells)_scatter.fcs FALSE lymphocytes
gate.define is.viability
1 TRUE FALSE
2 TRUE FALSE
3 TRUE FALSE
4 FALSE FALSE
5 TRUE TRUE
6 FALSE FALSE
Saving the Control File
Unfortunately, we are not currently able to pass the data.frame object directly to the AutoSpectral functions, so we will need to save our changes as an updated .csv file. This does have the benefit of permitting us to reuse the Control File again in the future. As always, saving under a new name to the working directory is recommended
UpdatedName <- "fcs_control_file_updated.csv"
StoreHere <- UpdatedName # For You
write.csv(ControlFile, StoreHere, row.names=FALSE) # For YouChecking the Control File
With the Control File now generated and edited, we can pass it to the check.control.file() function to verify that it is correctly formatted so that we can proceed to the next step. In our case, once we fixed the 7-AAD YG4 column, we had no issues and could proceed.
control.file <- StoreHere
check.control.file(control.dir, control.def.file=control.file, asp)
Gate Landmarks
Autospectral utilizes the same approach for automated gating as autospill, simply, fracturing the plot area, identifying areas of density, and iteratively honing in. While this can often work, as is always the case with flow cytometry, there are exeptions.
One way to work around this issue is to use the define.gate.landmarks() function, which references the Control File, identifies the peak detectors of fluorophores that are set to use a particular gate.name, and identifies the FSC-SSC location for cells with the brightest MFI for these designated detectors. This functionally serves as a landmark around which the gate is then placed.
For our example below, gate.name=“lymphocytes” is used by most of the fluorophores, which are utilized in the cross-check. Consequently, when we run define.gate.landmarks() for “lymphocytes”, we see the following
gate.lymphocyte <- define.gate.landmarks(
control.file = control.file,
control.dir = control.dir,
asp = asp,
n.cells = 2000,
percentile = 70,
gate.name = "lymphocytes"
)This results in a new folder being created in the working directory, containing both the gating landmark parameters

As well as the corresponding plot for each gate.name

Similarly, 7-AAD is the only fluorophore used with gate.name=“dead”, so the location of the brightest YG4-A cells ends up serving as the the landmark.
gate.dead <- define.gate.landmarks(
control.file = control.file,
control.dir = control.dir,
asp = asp,
n.cells = 2000,
percentile = 70,
gate.name = "dead"
)
Since both plots match my expectations of the location of “lymphocyte” and “dead” cells for this dataset, we can proceed to the next step.
Define Flow Control
With these initial structural elements assembled, AutoSpectral focus shift to the process of preparing the single-colors for signature isolation. Two important pre-processing steps that are utilized to ensure this is carried out properly. One of these is to match the gated area of the positive single-color control with the corresponding gating area in the negative unstained control. This “hopefully” would ensure that the background autofluorescence being substracted comes from the same cell type(s) and is more-similar-than-not.
To do this, we first need to assemble a list of the gate.names that are present for our panel. In our case, we only defined “lymphocytes” and “dead”, so we will only include those. Vice versa, if you also had a “monocyte” and “bead” gated region, you would include these as well.
my.gates <- list(
"lymphocytes" = gate.lymphocyte,
"dead" = gate.dead
)We then pass this “my.gates” list, our “asp” list, and the edited Control File to the define.flow.control() function. This then orchestrates the matching and drawing of gates for each fluorophore.
flow.control <- define.flow.control(
control.dir = control.dir,
control.def.file = control.file,
asp = asp,
gate.list = my.gates,
color.palette = "rainbow"
)
We end up with a flow.control variable, which consist of . Additionally, we also have a figure_gate folder containing the visualized plots for the individual fluorophores. Lets take a closer look
7-AAD


BV510


BV605


Alexa Fluor 700


Spark Blue 550


And autofluorescence for live cells

And autofluorescence for dead cells

As you may notice, the FSC-SSC coordinates being used by both the positive single-color and negative unstained are matching for subsequent use in the signature isolation.
Clean Control
The other important step in preparing the single-colors for signature isolation is identifying and separating out from the fluorophore-stained cells any variant autofluorescence that might be present in the .fcs files. This is an especially important step for cells where multiple mixed autofluorescence signatures may be present (ex. lung)
This is mediated through the use of the clean.controls function, to which we pass our flow.control and asp objects.
flow.control <- clean.controls(flow.control, asp) # Figure Clean Controls
As we can see from the output, clean.controls() first identifies and removes the variant autofluorescence, before generating the visual plots. It then gathers the scattered-matched negatives for the respective fluorophore to calculate out the signature now that potential variant autofluorescence is no longer present.
In the case of this dataset (originating from spleen), the effect was not as obvious for many of the fluorophores, although we can see the effect for BV510. From the plotted region, we can see the variant autofluorescence cells that were removed present in the gated region

And looking at the resulting impact on the signature, we see we end up with less outlier contributions for the Blue, Yellow-Green and Red detectors.

Spectra
Now that the single-color unmixing controls are matched to the corresponding FSC-SSC coordinates of the unstained, and variant autofluorescences are removed, AutoSpectral proceeds to calculate the fluorescent spectral signatures for each, before assembling them into a signature matrix.
spectra <- get.fluorophore.spectra(flow.control, asp) #Figure Scatter #Figure Spectra
If the signature is available via the reference database, a quick QC is carried out to make sure the signature is comparable to the reference (helping to screen for mistaken identity issues, etc).
If we check our spectra object, we can see the underlying data for each signature. This is also saved to the “table_spectra” folder containing the .csv file
SpectraTablePath <- "table_spectra" # For You
SpectraCSV <- list.files(path=SpectraTablePath,
pattern="Clean_autospectral_spectra.csv", full.names=TRUE) # For You
SpectraCSVData <- read.csv(SpectraCSV, check.names=FALSE)SpectraCSVData UV1-A UV2-A UV3-A UV4-A
1 BV510 -2.727532e-04 -3.382485e-05 0.0005592826 0.0025985403
2 BV605 -9.023060e-05 3.482036e-05 0.0023647498 0.0023766431
3 Spark Blue 550 -4.664226e-04 -1.621486e-04 0.0002002816 0.0007858372
4 Alexa Fluor 700 -1.881958e-04 9.158592e-06 0.0003507010 0.0007739788
5 7-AAD 4.479615e-05 -1.048530e-04 -0.0002540220 -0.0009888354
6 AF 9.285824e-03 8.175719e-02 0.2065093191 0.2575801443
UV5-A UV6-A UV7-A UV8-A UV9-A UV10-A
1 0.014476551 0.076312149 0.430678378 0.367125047 0.301704576 1.103656e-01
2 0.002549538 0.003343821 0.003284651 0.002022860 0.084943349 1.845648e-01
3 0.001238263 0.001390762 0.006568148 0.039078735 0.031286668 9.298637e-03
4 0.001455320 0.002269078 0.003395253 0.002123983 0.000990292 8.522156e-05
5 -0.001007894 -0.001126721 -0.002050312 -0.002276733 -0.001133780 8.736220e-03
6 0.354575952 0.603038095 1.000000000 0.531879836 0.331300916 1.051245e-01
UV11-A UV12-A UV13-A UV14-A UV15-A UV16-A
1 0.0400750959 0.019110130 0.013358563 0.0126528138 0.008946730 0.006540448
2 0.0929183866 0.038783175 0.029543802 0.0276185552 0.016988040 0.012089164
3 0.0033786183 0.001956591 0.001129301 0.0007983474 0.001182297 0.001294245
4 0.0003459306 0.007837035 0.037884246 0.0384602303 0.020973033 0.016098384
5 0.0239083922 0.016201002 0.013551166 0.0145070516 0.009922581 0.007450461
6 0.0176952153 0.010238330 0.005517397 0.0253328847 0.009068066 0.010576350
V1-A V2-A V3-A V4-A V5-A
1 1.847474e-03 0.0285723458 0.1196928499 0.2621554667 0.7017670004
2 7.264362e-02 0.0956278322 0.0802056499 0.0422455390 0.0208040332
3 -5.172626e-04 0.0007443447 0.0012103227 0.0014828872 0.0059244613
4 4.176814e-05 0.0003840570 0.0007235936 0.0008636901 0.0007257344
5 -8.005486e-04 -0.0007276090 -0.0008410830 -0.0001500675 -0.0007979136
6 -2.964998e-02 0.0182520797 0.0821702478 0.0855044828 0.0958970237
V6-A V7-A V8-A V9-A V10-A
1 0.7312399855 1.000000000 0.6933307372 0.4419144303 0.4440643313
2 0.0114675843 0.011798299 0.2003354839 0.8012451754 1.0000000000
3 0.0866516438 0.309186127 0.1828432668 0.1154995436 0.1031594150
4 0.0008416681 0.001297961 0.0002556033 0.0004298069 0.0003652906
5 -0.0010652055 -0.001397394 0.0029886726 0.0174935965 0.0902533906
6 0.0902867878 0.132868883 0.0635116152 -0.0182831414 -0.0305545839
V11-A V12-A V13-A V14-A V15-A V16-A
1 0.150744333 0.06931156 0.05128300 0.03504908 0.024946168 0.010957289
2 0.478537431 0.19407234 0.15238730 0.10253031 0.062249683 0.026565075
3 0.036138494 0.01743748 0.01451429 0.01104910 0.008375119 0.004085326
4 0.001719819 0.03212282 0.17949690 0.12830301 0.072554405 0.035549615
5 0.157054702 0.10394702 0.09250501 0.07167309 0.051951973 0.024509230
6 -0.048581519 -0.03750351 -0.02644606 -0.01535003 -0.019775754 -0.015461524
B1-A B2-A B3-A B4-A B5-A
1 0.0039019699 0.0035053550 0.0042274099 0.0018772712 0.0019232280
2 0.0008293361 0.0007234285 0.0010411823 0.0071584263 0.0389957545
3 0.0411008138 0.3615758919 1.0000000000 0.4102007805 0.3033972624
4 0.0009120344 0.0009217702 0.0008765814 0.0002866149 0.0005778189
5 -0.0014361667 -0.0010063697 -0.0012726204 0.0081649415 0.0444815039
6 0.0164939325 0.0070549490 0.0031785595 0.0106914127 -0.0223944522
B6-A B7-A B8-A B9-A B10-A
1 0.0009962270 0.0008127311 3.940405e-05 7.079352e-05 -3.218395e-05
2 0.0378572526 0.0276081132 1.757415e-02 1.366615e-02 8.307650e-03
3 0.1835399256 0.0966141674 6.630054e-02 6.210236e-02 3.556668e-02
4 0.0001344901 0.0001341962 7.820139e-04 8.880020e-03 2.289341e-02
5 0.1647852305 0.4321371020 3.676796e-01 3.914654e-01 2.421080e-01
6 -0.0357521689 -0.0191535072 -1.062635e-02 -3.160266e-02 -2.199200e-02
B11-A B12-A B13-A B14-A YG1-A
1 1.489764e-05 -8.466971e-05 3.118002e-05 -0.0002547132 0.0003706523
2 5.040537e-03 3.541526e-03 2.248089e-03 0.0027513159 0.0714330985
3 2.273251e-02 1.909118e-02 1.384848e-02 0.0165223523 0.0175283393
4 1.697353e-02 9.851103e-03 6.718159e-03 0.0078501671 0.0001749518
5 1.556714e-01 1.347122e-01 9.894565e-02 0.1103599829 0.0269825093
6 -1.167955e-02 1.951347e-03 -5.360082e-03 -0.0190680221 -0.0129333498
YG2-A YG3-A YG4-A YG5-A YG6-A
1 3.181876e-04 0.0002811334 0.000132580 -0.0001742363 -0.0001239778
2 3.471537e-01 0.3573060937 0.228378511 0.1373171497 0.0931814627
3 1.048847e-02 0.0073584483 0.002753506 0.0016306145 0.0016885876
4 4.390731e-05 0.0004871955 0.001068177 0.0062230267 0.0479852083
5 1.022791e-01 0.3225571974 1.000000000 0.8107526645 0.7465303727
6 -2.698419e-02 -0.0413665828 -0.045529362 -0.0288697062 -0.0343305659
YG7-A YG8-A YG9-A YG10-A R1-A
1 -0.000334327 0.0002804432 -0.0002453722 -0.0003688434 5.359081e-05
2 0.103676774 0.0405542259 0.0240471093 0.0137332944 9.999626e-04
3 0.001162221 0.0007593936 -0.0007598628 0.0001050383 7.382258e-05
4 0.280107280 0.1230369156 0.0702979614 0.0465641625 2.949726e-03
5 0.897883288 0.4140019657 0.3063294830 0.1890652140 1.388792e-02
6 -0.044537130 -0.0176013392 -0.0123816675 -0.0041210102 2.991331e-03
R2-A R3-A R4-A R5-A R6-A
1 5.759475e-05 -2.368085e-05 0.0001559280 7.930201e-05 1.512069e-05
2 7.856179e-04 7.895259e-04 0.0006935048 3.872853e-04 2.709521e-04
3 5.420816e-04 2.617683e-04 -0.0006480708 -2.521269e-04 1.842664e-04
4 3.938425e-02 3.640583e-01 1.0000000000 9.106755e-01 4.514638e-01
5 1.913133e-02 1.903504e-02 0.0143610777 1.116117e-02 7.917969e-03
6 -9.235466e-03 -1.335063e-02 -0.0200660886 -1.634608e-03 -1.056407e-02
R7-A R8-A
1 3.247486e-06 -2.443376e-04
2 2.691855e-04 2.996067e-05
3 -5.194820e-04 -5.025562e-04
4 4.274690e-01 2.465092e-01
5 8.028963e-03 3.941527e-03
6 -1.515120e-02 2.730999e-03
In the process, the plotted figures are stored to the “figure_spectra” and “figure_similarity” folders. Among the plots of interest, we get back the plotted normalized spectral signatures

A similarity matrix containing the cosine values when comparing one fluorophore against another

A spectral heatmap showing intensity across detectors for each fluorophore (corresponding to the peak detector and relative height of the other detectors)

And the coefficients, which will play a role later on.

Additionally, there is a .pdf report with the plotted extracted signatures vs. the reference

Unmixing
Now that all the pre-requisites have been assembled, we can now leverage them to unmix the full-stained .fcs files. To do this, lets first provide the file path to their respective folder.
FullStainedPath <- file.path(StorageLocation, "samples")Additionally, lets drop the row containing the AF from the spectra object, as different unmixing methods will handle the autofluorescence differently.
rownames(spectra)
no.af.spectra <- spectra[ !(rownames(spectra) == "AF"),]
rownames(no.af.spectra)OLS
AutoSpectral has 4 ways to perform unmixing. One of these is Ordinary Least Squares (OLS), which is what most of the spectral flow cytometry unmixing R packages have as a default. While the option exist to unmix individual files, I prefer to use the unmix.folder function to unmix all at the same time. When AutoSpectralRcpp is installed, we can take advantage by setting parallel equal to TRUE, and specifying the number of core threads to allocate for the process (this will vary based on your computers CPU specifications).
unmix.folder(
fcs.dir = FullStainedPath,
spectra = spectra,
asp = asp,
flow.control = flow.control,
method = "OLS",
parallel = TRUE,
threads = 3,
output.dir=OutputLocation
)We can then quickly check to make sure everything looks normal to other users not using R for their analysis by opening the resulting .fcs files using commercial software or Floreada.io

Oops! Looks like somewhere in the description/keyword list AutoSpectral still needs to have the P–DISPLAY keyword switched from Lin to Log. However, we can manually switch the axis over and apply a transformation, at which thing everything is back to looking as expected


A small unmixing error for CD4+ T cells which are BV510 negative, but otherwise not bad considering this is a small not-complicated panel. On to the next method!
WLS
AutoSpectral also provides the option to unmix using Weighted Least Squares. For most cytometers, it mediates this by estimating residual variance across the detectors for each of the single colors (see pre-print for the technical implementation details).
The setup is essentially the same as with OLS, just switching out the method to instead use “WLS”
unmix.folder(
fcs.dir = FullStainedPath,
spectra = spectra,
asp = asp,
flow.control = flow.control,
method = "WLS",
parallel = TRUE,
threads = 3,
output.dir=OutputLocation
)

In this case, the unmixing error on the CD4+ T cells is a little less noticeable than previously.
Per-cell AF extraction
Beyond better signature isolation from the single-color controls (by negative matching and removing variant autofluorescence in advance), AutoSpectral attempts to handle the issue of multiple autofluorescence by isolating autofluorescence signatures from the unstained sample, and then determining at the individual cell level what autofluorescences are likely present. See the pre-print for all the technical details behind the implementation. Namely, signatures are isolated, FlowSOM is utilized to identify the likely troublemakers, and then through iterative process what present individual cell determined via identifying least absolute variance for low-squared residuals.
In practice, we need to provide the file.path to our unstained .fcs file to the get.af.spectra function. This unstained .fcs file path should match that of the corresponding tissue type as the full-stained sample being unmixed.
UnstainedPath <- file.path(StorageLocation, "unmixing_controls",
"Unstained (Cells)_scatter.fcs")
spleen.af <- get.af.spectra(
unstained.sample = UnstainedPath,
asp = asp,
spectra = spectra,
refine = TRUE
)When this is then, we get back a few plot outputs to the autofluorescence folder. We can see all 100 unstained signature variants that ended up in the FlowSOM

We can also visualize at which detectors these differences are most manifested.

And via the heatmap get a rough idea of the overall heterogeneity gradient

Additionally, we can see how the iteration process of identifying autofluorescences for use in the unmixing plays out as the variance around the negative decreases going from no AF extraction, to first pass and second pass.
.fcs _ Autofluorescence spectra _No_AF_Extraction.jpg)
.fcs_Autofluorescence spectra_PerCell_AF_Extraction_First_Pass.jpg)
.fcs_Autofluorescence spectra_PerCell_AF_Extraction_Second_Pass.jpg)
Additionally, we get back a .csv contianing the fluorescent signatures for each of the variant autofluorescence signatures, which can be used for after-the-fact unmixing.
To proceed, we modify the unmix.folder() function by switching method to “AutoSpectral”, and providing some addition arguments.
unmix.folder(
fcs.dir = FullStainedPath,
spectra = spectra,
asp = asp,
flow.control = flow.control,
method = "AutoSpectral",
af.spectra = spleen.af,
file.suffix = "per-cell AF extraction",
parallel = TRUE,
threads = 3,
output.dir=OutputLocation
)
And last but not least, you can factor fluorophore signature variation as well.
With everything successfully run, we can proceed and visualize the result on unmixing


Fluorophore optimization and per-cell AF
Finally, some of the variation in unmixing may be due to the fluorophores themselves shifting to a small degree (think of a smaller branch breaking off a larger tandem fluorophore). As long as above the cutoff (>0.98 on cosine) AutoSpectral will attempt to estimate on a per-cell basis via a simialr mechanisms as was implemented to determine the autofluorescent variants.
variants <- get.spectral.variants(
control.dir = control.dir,
control.def.file = control.file,
asp = asp,
spectra = spectra,
af.spectra = spleen.af,
parallel = FALSE,
refine = TRUE
)
This in turn results with a compilation of signature variants for the individual fluorophores, which then gets integrated into the unmixing process as documented in the pre-print. For our dataset, individual fluorophores varied a bit in the extent this was an issue, as can be seen from the saved plots




To run with per-cell AF extraction and fluorophore optimization, we still keep the method set to “AutoSpectral”, but also provide a spectra.variants argument, passing in our variants object.
unmix.folder(
fcs.dir = FullStainedPath,
spectra = spectra,
asp = asp,
flow.control = flow.control,
method = "AutoSpectral",
af.spectra = spleen.af,
spectra.variants = variants,
file.suffix = "per-cell AF and fluorophore optimization",
parallel = TRUE,
speed="slow",
threads = 3,
output.dir=OutputLocation
)
With the unmixing complete, we can now visualize the output .fcs files to contrast against the other unmixing methods.


Take Away
In this walk-through, we show-cased a simple implementation of the AutoSpectral workflow. Unlike many of the other R packages we have covered during the course, AutoSpectral is in no shortage of documentation, so I highly recommend checking out the additional resources links under the Additional Resources section.



Additional Resources
AutoSpectral GitHub repository GitHub repository for AutoSpectral. Oliver Burton is active when it comes to both documentation and addressing any bugs encountered via the repository Issues page.
Autospectral Vignettes The pkdgown website with a total of 17 vignette articles covering the various finer details of how to use AutoSpectral.
AutoSpectral Pre-Print The pre-print article describing the underlying methodology and all the finer details I am not qualified to explain.
Colibri Cytometry Additional blogpost explainers that Oliver has written about AutoSpectral, some overlap with the vignettes, some completely new.


