The purpose of flow cytometry is to make inferences regarding some
cell type(s) of interest. Often this involves establishing/drawing a
gating hierarchy to sequentially filter down to the cell type(s) of
interest. This process is very often done manually, and can be very
labour intensive. Importantly, manual approach implies large
variation when one person does it vs. the next, or even if the
same person does it multiple times. The cytoverse
offers a
suite of tools to tackle this problem in a reproducible and programmatic
manner.
In the previous sections, we have seen (and worked with)
cytoframe
, and cytoset
. These objects hold the
underlying data, allowing us to visualize it, manipulate it, etc.
In this section we work with GatingSet
and
GatingHierarchy
. These objects, like the name suggests,
store information regarding various gates and filters that we will
generate. Importantly, we can save the GatingSet
which will
completely package the analysis as well as the .fcs
files in an opensource format that can be shared, allowing
reproducibility.
In part 1 of this section, we will go over variety
of native methods that exist in the cytoverse
which
analysts can utilize to gate their cells of interest.
In part 2 of this sections, we will go over methods to extract gated data for cells/populations of interest.
In part 3 (optional), we will demonstrate how
analysts can automate this process by utilizing a
gatingTemplate
(a csv file that can be used to define and
build the hierarchy).
GatingSet
Required libraries
library(flowWorkspace)
library(ggcyto)
# set ggcyto theme
theme_set(theme_classic())
library(CytoverseBioc2023)
## Warning: replacing previous import 'flowViz::contour' by 'graphics::contour'
## when loading 'flowStats'
To create a GatingSet
, first load in a
cytoset
. Here, we are making use of the
cytoset
that we have created previously.
# load cytoset
cs <- make_cytoset(only_TNK = TRUE)
# creating a GatingSet
gs <- flowWorkspace::GatingSet(cs)
gs
## A GatingSet with 4 samples
We have now created a GatingSet
called
gs.
GatingSet
?gs
to only include samples where
Treatment = Control?The GatingSet
needs to be compensated
and transformed before we start attaching gates.
Note: The approach to compensate and
transform a GatingSet
is similar to that
of cytoframe
and cytoset
; As such, we ask you
to run the code below, up to line 125.
We can directly compensate the GatingSet
object.
# compensate a GatingSet
spill <- keyword(gs[[1]],"$SPILLOVER") # extract spillover matrix stored within the file
gs <- compensate(gs,spill) # GatingSet will be compensated and stores the spill matrix as well
recompute(gs) # update the gs
# retrieve compensation information
gs_spill <- gs_get_compensations(gs[[1]])
# output is a rich spillover information
slot(gs_spill[[1]],"spillover")[1:4, 1:4]
## B515-A B610-A B660-A B710-A
## B515-A 1.000000000 0.07573059 0.04266579 0.003687143
## B610-A 0.009777704 1.00000000 0.78589800 0.074328487
## B660-A 0.005482375 0.03108691 1.00000000 0.112570733
## B710-A 0.024698965 0.12699130 0.90643808 1.000000000
As well we can transform the GatingSet
.
For convenience, we are using a previously defined transformerList object. The transformerList object was extracted from the workspace file created by the authors of this study and can be found here.
# transformation of GatingSet
t.rds <- dplyr::filter(
get_workshop_data("/fj_wsp"), # a previously defined transformation
grepl(
pattern = "transform",
x = rname
)
)$rpath
my_trans <- readRDS(file = t.rds)
# make into transformerList object
my_trans_list <- flowWorkspace::transformerList(
from = names(my_trans),
trans = my_trans
)
gs <- flowWorkspace::transform(gs, my_trans_list) # transforms underlying data
GatingHierarchy
You may have noticed above that some function calls have
gs_
and others have gh_
.
gs_
indicates that GatingSet
while
gh_
indicates a GatingHierarchy
.
The GatingHierarchy
is a data structure that stores the
sample-wise gating information present in a GatingSet
. In
essence a GatingSet
is a collection of
GatingHierarchy
.
You can access the GatingHierarchy
using the
[[
subset operation.
On this GatingSet
, we will add various gates that
identifies cells of interest. Each sample within a
GatingSet
is associated with a GatingHierarchy
that stores information regarding various gates that we would have
created and applied to the samples.
Below, we demonstrate various types of gates that could be estimated
automatically or be defined
programmatically which can be applied to the samples
within a GatingSet
to build a hierarchy.
Note: This process is informed by
visualization of the data. As such, we will make ample
use of the ggcyto
library.
The gs_add_gating_method
call from openCyto
library can be leveraged for automatic estimation of gates, as well as
clean scripting! Available gating methods can be listed by calling
gt_list_methods()
. Please see additional examples here: cytoverse.
List of available methods.
## Gating Functions:
## === quantileGate
## === gate_quantile
## === rangeGate
## === flowClust.2d
## === gate_flowclust_2d
## === mindensity
## === gate_mindensity
## === mindensity2
## === gate_mindensity2
## === cytokine
## === flowClust.1d
## === gate_flowclust_1d
## === boundary
## === singletGate
## === quadGate.tmix
## === gate_quad_tmix
## === quadGate.seq
## === gate_quad_sequential
## Preprocessing Functions:
## === prior_flowClust
## === prior_flowClust
## === warpSet
## === standardize_flowset
A singlets
gate is mostly used for data clean up, by
removing out doublets
.
# visualization
singlet_vis <- autoplot(
gs_pop_get_data(gs,"root"), # gs_pop_get_data(gs, node) extracts the underlying data
x = "FSC-A",
y = "FSC-H",
bins = 256
) +
facet_wrap(~sampleNames(gs)
) # leverage the metadata that is saved within the GatingSet to facet plots
singlet_vis
Now, we estimate and add!
# estimate and add
gs_add_gating_method(
gs = gs, # gatingset
alias = "singlets", # name given to the population
pop = "+", # indicate whether events inside or outside the gate should be filtered
parent = "root", # where to attach this node
dims = "FSC-A,FSC-H", # dimensions used to estimate the gate
gating_method = "singletGate", # one of the available gating methods
gating_args = "wider_gate = FALSE", # arguments passed to singletGate
)
# visualize
singlet_vis +
geom_gate(
gs_pop_get_gate(
gs,
"singlets"
)
)+
facet_wrap(~name)
gate_quantile
and
gate_mindensity2
gate_quantile
and gate_mindensity2
can be
used to estimate a cut point in the data, allowing the gating of data to
the left(-) or the right(+) of the cut
point.
# calculate a live gate
## Example of a quantile gate
## visualize
live_viz <- ggcyto(
gs,
subset = "singlets",
aes(x = "Live", y = "FSC-A")
) +
geom_hex(bins = 128)+
facet_wrap(~name)
# live_viz
# add gate using gate_quantile
gs_add_gating_method(
gs,
alias = "live",
pop = "-",
parent = "singlets",
dims = "U450-A",
gating_method = "gate_quantile",
gating_args = list(
probs = 0.95
)
)
# add gate using gate_mindensity2
gs_add_gating_method(
gs,
alias = "live_mindensity",
pop = "-",
parent = "singlets",
dims = "U450-A",
gating_method = "gate_mindensity2",
gating_args = list(
max = 100,
gate_range = c(50, 75)
)
)
# visualize the 2 extimated gates
live_viz+
geom_gate(gs_pop_get_gate(gs, "live"), colour = "red")+
geom_gate(gs_pop_get_gate(gs, "live_mindensity"), colour = "blue")
We can also use the cutpoints to estimate a rectangular gate!
# conventional T cells
t_cell_vis <- ggcyto(
gs, subset = "live",
aes(x = "TCR Va7_2", y = "CD161")
) + geom_hex(bins = 256)+
facet_wrap(~name)
# visualize
# t_cell_vis
# estimate and add
gs_add_gating_method(
gs,
alias = "MAIT Cells",
pop = "++",
parent = "live",
dims = "G660-A,V710-A",
gating_method = "gate_quantile",
gating_args = list(
probs = 0.95,
min = 50,
max = 200
)
)
## ...
## done
# visualize
t_cell_vis+
geom_gate(gs_pop_get_gate(gs,"MAIT Cells"))
gate_flowclust_2d
can be used to identify and generate
an Ellipsoid gate
in a semi-supervised manner.
cd3_ellipse <- ggcyto(gs,
subset = "live",
aes(x = "CD4", y = "CD3"))+
geom_hex(bins = 256)
# estimate and add
gs_add_gating_method(
gs,
alias = "CD3+",
pop = "+",
parent = "live",
dims = "U785-A,V510-A",
gating_method = "gate_flowclust_2d",
gating_args = list(
K = 3,
target = c(100,150),
quantile = 0.9,
plot = FALSE
)
)
# visualize
cd3_ellipse+
geom_gate(gs_pop_get_gate(gs,"CD3+"))
CD4+ T Cells
defined
as: CD4+ and CD3+?gating_args
above indicates
K = 3, quantile = 0.9, plot = FALSE
. Try running the code
block by indicating plot = TRUE
. What is the outcome? How
can you run this code block again? Hint: try running
help.search(pattern = "remove gating")
.When there are population(s) of interest in all 4 quadrants, it is
useful to estimate a quadrant gate
using
gate_quad_sequential
. Note: Pay attention to
alias
and pop
arguments!
# plot subsets
t_subsets <- ggcyto(
gs,
subset = "CD3+",
aes(x = "CD45RA", y = "CCR7")
)+
geom_hex(bins = 256)+
facet_wrap(~name)
# visualize
# t_subsets
# estimate and add
gs_add_gating_method(
gs,
alias = "*",
pop = "+/-+/-",
parent = "CD3+",
dims = "CD45RA,CCR7",
gating_method = "gate_quad_sequential",
gating_args = list(
gFunc = "mindensity"
),
collapseDataForGating = TRUE
)
gs_add_gating_method
? Hint: Try running
help(gs_add_gating_method)
and read the details.
gs_remove_gating_method
reverses the the results of
gs_add_gating_method
in a step wise manner.
gs_add_gating_method
? How
can you use them?Note: gs_add_gating_method
keeps a history of
the calls made. If you reload a previously saved GatingSet
via load_gs
to work on, start with a clean history using
gs_add_gating_method_init(gs)
so that errors do not
arise.
Suppose you would like to add a gate that has a particular shape or range that cannot be easily estimated using the automatic estimation approaches above. In this case, you could programmatically define a gate by either providing ranges, coordinates, or logical statements as demonstrated below.
As the name suggests, we can explicitly define a
rectangular gate
by by defining a matrix
where
the columns are the ranges for markers in x-axis
and
y-axis
. Only 1 dimension can also be provided. In this
scenario, the constructed gate filter the events marginated over the
dimension provided.
## Example of rectangleGate
cd3_vis <- ggcyto(
gs, subset = "root", aes(x = "CD3", y = "CD56")
)+
geom_hex(bins = 256)+
facet_wrap(~name)
# before
# cd3_vis
# using rectangle gate to add T cell gate
cd3_rectangle <- matrix(
c(140, 205, 0, 200),
nrow = 2,
ncol = 2,
byrow = F,
dimnames = list(
c("min", "max"), # rownames
c("V510-A","U570-A") # colnames are channel names
)
)
cd3_rectangle_gate <- rectangleGate(
.gate = cd3_rectangle,
filterId = "CD3+ T cells"
)
cd3_vis+geom_gate(cd3_rectangle_gate)
When a single dimension is provided:
# provide the range
cd3_range <- matrix(data = c(140,200))
# set the column name to the required channel
colnames(cd3_range) <- "V510-A"
# construct gate
cd3_range_gate <- rectangleGate(
.gate = cd3_range,
filterId = "CD3+ Range"
)
cd3_vis + geom_gate(cd3_range_gate)
To add this gate we call gs_pop_add
like so:
gs_pop_add(
gs,
parent = "live",
gate = cd3_range_gate
)
## [1] 13
Each call to gs_pop_add
requires a
recompute(gs)
call.
Similar to Rectangular gate
we can define a
Polygon gate
by generating a matrix
of
vertices.
Here, each row of the matrix is the coordinate for a vertex and the columns are the channels.
## Example of polygonGate
nkt_vis <- ggcyto(gs, subset = "root",
aes(x = "CD1d", y = "CD3"))+ # fuzzy matching of marker names
geom_hex(bins = 256)+
facet_wrap(~name)
## Warning in getChannelMarker(frm, dim): CD1d is partially matched with
## R670-ACD1d:PBS57 tet APC
# visualize
# nkt_vis
# define coordinates
## coordinates are based on visualization!
nkt_poly <- matrix(
c(
115,140, # are arranged as x,y pair
150,150,
150,180,
200,180,
200,140
),
ncol = 2,
byrow = T, # indicates that add as x,y pair
dimnames = list(
NULL, # rownames have no meaning in polygonGates
c("R670-A","V510-A") # colnames are channel names
)
)
# create a gate
nkt_poly_gate <- polygonGate(
nkt_poly,
filterId = "NKT cells"
)
# visualize
nkt_vis + geom_gate(nkt_poly_gate)
Editing a gate after construction is straight forward.
# move up and scale
nkt_poly_gate_scale <- flowCore::transform_gate(
nkt_poly_gate, # gate object
dx = 1, # which dimension to shift and by how much
# dy = 1,
# scale = 2 # scales both dimensions equally
scale = c(1.05,1.05) # individually scale each dimension
)
nkt_vis+geom_gate(nkt_poly_gate_scale)
Rectangular gate
that can be used to
filter CD4- CD3+ population. Hint: Visualize
the data and then define the best ranges.help(flowCore::transform_gate)
.CD3+
gate we created using
gs_add_gating_method
, how would you edit it? Hint:
try running help.search("get or set gate")
.We can use booleanFilter
to simply
negate a gated population! The resulting filter does
not have a geometric representation.
# add not MAIT gate
## Example of booleanFilter
not_mait <- booleanFilter(`!MAIT Cells`, filterId = "not_MAIT")
# add Boolean gate
gs_pop_add(
gs,
not_mait,
parent = "live"
)
## [1] 14
recompute(gs)
# visualize
t_cell_vis+
geom_overlay(
gs_pop_get_data(gs, y = "not_MAIT"),
size = 0.3,
colour = "red"
)
Note: We are also able to combine multiple gates to generate
a booleanFilter
.
Let’s create by combining not_MAIT
and
MAIT Cells
# using an OR ('|') operator
all_cells <- booleanFilter(
`not_MAIT|MAIT Cells`,
filterId = "All"
)
# add population
gs_pop_add(gs, all_cells, parent = "live")
recompute(gs)
booleanFilter
that only filters
out naive T cells defined as
CD45RA+CCR7+? Can you visualize the result?All
?recompute
required after removal of a gate?We have been creating and adding many gates to our
GatingSet
. A useful way to visualize all the gates and
their relationship is by plotting the gating Hierarchy as a tree. This
gives us an immediate summary of what nodes are present in our
GatingSet
.
# plot the gating tree
plot(gs, bool = TRUE)
Another useful approach is to visualize the gated data, with gates that we have generated. See the section of visualization for more.
# visualize the full gating hierarchy
autoplot(
gs[[1]],
bins = 256,
bool = TRUE)+
ggcyto_par_set(limits = "data") # set data range to be determined by data
Finally, we save your GatingSet
. The
GatingSet
can be loaded back into R using
load_gs("path/to/a/folder")
.
# save your work
save_gs(gs, path = "path/to/a/folder")
In this section, we spent some time identifying various types of
gates that are available in cytoverse
. As well, we
demonstrated how to programmatically create such gates, either manually
(i.e. by defining ranges or vertices) or in a semi-automated and data
driven manner.
Next, we go over methods to extract the gated data.
The (Optional) Part
3 goes over how users can leverage a
gatingTemplate
to generate gates as we have done here, but
with minimal scripting. It is worth noting that while the use
gatingTemplate
minimizes scripting, it does not compromise
on reproducibility!
Lastly, we also made ample use of the ggcyto
library in
order to generate visualizations that helped in QC’ing the data and the
gates that we generated. We will go over ggcyto
in more
detail in Visualizations.