The R markdown is available from the pulldown menu for Code
at the upper-right, choose “Download Rmd”, or download
the Rmd from GitHub.
This vignette will show how to perform basic network operations on an
iGraph networks and use this information to customize its appearance in
Cytoscape directly from R using the RCy3 package
*From Vessy’s “Fun with R blog”: http://www.vesnam.com/Rblog/viznets5/
Installation
if(!"RCy3" %in% installed.packages()){
install.packages("BiocManager")
BiocManager::install("RCy3")
}
library(RCy3)
if(!"igraph" %in% installed.packages()){
install.packages("igraph")
}
library(igraph)
if(!"plyr" %in% installed.packages()){
install.packages("plyr")
}
library(plyr)
Required Software
The whole point of RCy3 is to connect with Cytoscape. You will need
to install and launch Cytoscape:
Read a data set.
Data format: dataframe with 3 variables; variables 1 & 2
correspond to interactions; variable 3 is weight of interaction
lesmis <- system.file("extdata","lesmis.txt", package="RCy3")
dataSet <- read.table(lesmis, header = FALSE, sep = "\t")
Create a graph. Use simplify to ensure that there are no duplicated
edges or self loops
gD <- igraph::simplify(igraph::graph.data.frame(dataSet, directed=FALSE))
Verify the number of nodes (77) and edges (254):
igraph::vcount(gD)
igraph::ecount(gD)
Common iGraph functions
Calculate some node properties and node similarities that will be
used to illustrate different plotting abilities
Calculate degree for all nodes
degAll <- igraph::degree(gD, v = igraph::V(gD), mode = "all")
Calculate betweenness for all nodes
betAll <- igraph::betweenness(gD, v = igraph::V(gD), directed = FALSE) / (((igraph::vcount(gD) - 1) * (igraph::vcount(gD)-2)) / 2)
betAll.norm <- (betAll - min(betAll))/(max(betAll) - min(betAll))
rm(betAll)
Calculate Dice similarities between all pairs of nodes
dsAll <- igraph::similarity.dice(gD, vids = igraph::V(gD), mode = "all")
Add attributes to network
Add new node attributes based on the calculated node
properties/similarities
gD <- igraph::set.vertex.attribute(gD, "degree", index = igraph::V(gD), value = degAll)
gD <- igraph::set.vertex.attribute(gD, "betweenness", index = igraph::V(gD), value = betAll.norm)
Check the attributes. You should see “degree” and “betweeness” now,
in addition to “name”.
And now for the edge attributes…
F1 <- function(x) {data.frame(V4 = dsAll[which(igraph::V(gD)$name == as.character(x$V1)), which(igraph::V(gD)$name == as.character(x$V2))])}
dataSet.ext <- plyr::ddply(dataSet, .variables=c("V1", "V2", "V3"), function(x) data.frame(F1(x)))
gD <- igraph::set.edge.attribute(gD, "weight", index = igraph::E(gD), value = 0)
gD <- igraph::set.edge.attribute(gD, "similarity", index = igraph::E(gD), value = 0)
Note: The order of interactions in dataSet.ext is not the same as
it is in dataSet or as it is in the edge list and for that reason these
values cannot be assigned directly
for (i in 1:nrow(dataSet.ext))
{
igraph::E(gD)[as.character(dataSet.ext$V1) %--% as.character(dataSet.ext$V2)]$weight <- as.numeric(dataSet.ext$V3)
igraph::E(gD)[as.character(dataSet.ext$V1) %--% as.character(dataSet.ext$V2)]$similarity <- as.numeric(dataSet.ext$V4)
}
rm(dataSet,dsAll, i, F1)
Check the edge attributes. You should see “weight” and “similarity”
added.
Lets check it out in Cytoscape
Update: You can go straight from igraph to Cytoscape, sending all
attributes and displaying graph!
createNetworkFromIgraph(gD,new.title='Les Miserables')
Let’s decide on a layout
A list of available layouts can be accessed from R as follows:
We’ll select the “fruchterman-rheingold” layout. To see properties
for the given layout, use:
getLayoutPropertyNames("fruchterman-rheingold")
We can choose any property we want and provide them as a
space-delimited string:
layoutNetwork('fruchterman-rheingold gravity_multiplier=1 nIterations=10')
But that is a crazy layout, so let’s try “force-directed”
instead:
layoutNetwork('force-directed defaultSpringLength=70 defaultSpringCoefficient=0.000003')
Next, we can visualize our data
On nodes…
setNodeColorMapping('degree', c(min(degAll), mean(degAll), max(degAll)), c('#F5EDDD', '#F59777', '#F55333'))
lockNodeDimensions(TRUE)
setNodeSizeMapping('betweenness', c(min(betAll.norm), mean(betAll.norm), max(betAll.norm)), c(30, 60, 100))
…and edges
setEdgeLineWidthMapping('weight', c(min(as.numeric(dataSet.ext$V3)), mean(as.numeric(dataSet.ext$V3)), max(as.numeric(dataSet.ext$V3))), c(1,3,5))
setEdgeColorMapping('weight', c(min(as.numeric(dataSet.ext$V3)), mean(as.numeric(dataSet.ext$V3)), max(as.numeric(dataSet.ext$V3))), c('#BBEE00', '#77AA00', '#558800'))
We will define our own default color/size schema after we defined
node and edge rules, due to possible issues when using rules
setBackgroundColorDefault('#D3D3D3')
setNodeBorderColorDefault('#000000')
setNodeBorderWidthDefault(3)
setNodeShapeDefault('ellipse')
setNodeFontSizeDefault(20)
setNodeLabelColorDefault('#000000')
Voila! All done.
Track versions for your records
LS0tCnRpdGxlOiAiTmV0d29yayBmdW5jdGlvbnMgYW5kIHZpc3VhbGl6YXRpb24iCmF1dGhvcjogImJ5IEFsZXhhbmRlciBQaWNvIgpwYWNrYWdlOiBSQ3kzCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCIKb3V0cHV0OiAKICBodG1sX25vdGVib29rOgogICAgdG9jX2Zsb2F0OiB0cnVlCiAgICBjb2RlX2ZvbGRpbmc6ICJub25lIgojICBwZGZfZG9jdW1lbnQ6CiMgICAgdG9jOiB0cnVlIAp2aWduZXR0ZTogPgogICAgJVxWaWduZXR0ZUluZGV4RW50cnl7MDUuIE5ldHdvcmsgZnVuY3Rpb25zIGFuZCB2aXN1YWxpemF0aW9uIH4xNSBtaW59CiAgICAlXFZpZ25ldHRlRW5naW5le2tuaXRyOjpybWFya2Rvd259CiAgICAlXFZpZ25ldHRlRW5jb2Rpbmd7VVRGLTh9Ci0tLQpgYGB7ciwgZWNobyA9IEZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoCiAgZXZhbD1GQUxTRQopCmBgYAoqVGhlIFIgbWFya2Rvd24gaXMgYXZhaWxhYmxlIGZyb20gdGhlIHB1bGxkb3duIG1lbnUgZm9yKiBDb2RlICphdCB0aGUgdXBwZXItcmlnaHQsIGNob29zZSAiRG93bmxvYWQgUm1kIiwgb3IgW2Rvd25sb2FkIHRoZSBSbWQgZnJvbSBHaXRIdWJdKGh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9jeXRvc2NhcGUvY3l0b3NjYXBlLWF1dG9tYXRpb24vbWFzdGVyL2Zvci1zY3JpcHRlcnMvUi9ub3RlYm9va3MvTmV0d29yay1mdW5jdGlvbnMtYW5kLXZpc3VhbGl6YXRpb24uUm1kKS4qCgo8aHIgLz4KClRoaXMgdmlnbmV0dGUgd2lsbCBzaG93IGhvdyB0byBwZXJmb3JtIGJhc2ljIG5ldHdvcmsgb3BlcmF0aW9ucyBvbiBhbiBpR3JhcGgKbmV0d29ya3MgYW5kIHVzZSB0aGlzIGluZm9ybWF0aW9uIHRvIGN1c3RvbWl6ZSBpdHMgYXBwZWFyYW5jZSBpbiBDeXRvc2NhcGUgCmRpcmVjdGx5IGZyb20gUiB1c2luZyB0aGUgUkN5MyBwYWNrYWdlCgoqRnJvbSBWZXNzeSdzICJGdW4gd2l0aCBSIGJsb2ciOiBodHRwOi8vd3d3LnZlc25hbS5jb20vUmJsb2cvdml6bmV0czUvCgojIEluc3RhbGxhdGlvbgpgYGB7cn0KaWYoISJSQ3kzIiAlaW4lIGluc3RhbGxlZC5wYWNrYWdlcygpKXsKICAgIGluc3RhbGwucGFja2FnZXMoIkJpb2NNYW5hZ2VyIikKICAgIEJpb2NNYW5hZ2VyOjppbnN0YWxsKCJSQ3kzIikKfQpsaWJyYXJ5KFJDeTMpCmlmKCEiaWdyYXBoIiAlaW4lIGluc3RhbGxlZC5wYWNrYWdlcygpKXsKICAgIGluc3RhbGwucGFja2FnZXMoImlncmFwaCIpCn0KbGlicmFyeShpZ3JhcGgpCmlmKCEicGx5ciIgJWluJSBpbnN0YWxsZWQucGFja2FnZXMoKSl7CiAgICBpbnN0YWxsLnBhY2thZ2VzKCJwbHlyIikKfQpsaWJyYXJ5KHBseXIpCmBgYAoKIyBSZXF1aXJlZCBTb2Z0d2FyZQpUaGUgd2hvbGUgcG9pbnQgb2YgUkN5MyBpcyB0byBjb25uZWN0IHdpdGggQ3l0b3NjYXBlLiBZb3Ugd2lsbCBuZWVkIHRvIGluc3RhbGwgYW5kIGxhdW5jaCBDeXRvc2NhcGU6IAogICAgCiogRG93bmxvYWQgdGhlIGxhdGVzdCBDeXRvc2NhcGUgZnJvbSBodHRwOi8vd3d3LmN5dG9zY2FwZS5vcmcvZG93bmxvYWQucGhwCiogQ29tcGxldGUgaW5zdGFsbGF0aW9uIHdpemFyZAoqIExhdW5jaCBDeXRvc2NhcGUgCgpgYGB7cn0KY3l0b3NjYXBlUGluZygpCmBgYAoKIyBSZWFkIGEgZGF0YSBzZXQuIApEYXRhIGZvcm1hdDogZGF0YWZyYW1lIHdpdGggMyB2YXJpYWJsZXM7IHZhcmlhYmxlcyAxICYgMiBjb3JyZXNwb25kIHRvIGludGVyYWN0aW9uczsgdmFyaWFibGUgMyBpcyB3ZWlnaHQgb2YgaW50ZXJhY3Rpb24KYGBge3J9Cmxlc21pcyA8LSBzeXN0ZW0uZmlsZSgiZXh0ZGF0YSIsImxlc21pcy50eHQiLCBwYWNrYWdlPSJSQ3kzIikKZGF0YVNldCA8LSByZWFkLnRhYmxlKGxlc21pcywgaGVhZGVyID0gRkFMU0UsIHNlcCA9ICJcdCIpCmBgYAoKQ3JlYXRlIGEgZ3JhcGguIFVzZSBzaW1wbGlmeSB0byBlbnN1cmUgdGhhdCB0aGVyZSBhcmUgbm8gZHVwbGljYXRlZCBlZGdlcyBvciBzZWxmIGxvb3BzCmBgYHtyfQpnRCA8LSBpZ3JhcGg6OnNpbXBsaWZ5KGlncmFwaDo6Z3JhcGguZGF0YS5mcmFtZShkYXRhU2V0LCBkaXJlY3RlZD1GQUxTRSkpCmBgYAoKVmVyaWZ5IHRoZSBudW1iZXIgb2Ygbm9kZXMgKDc3KSBhbmQgZWRnZXMgKDI1NCk6CmBgYHtyfQppZ3JhcGg6OnZjb3VudChnRCkKaWdyYXBoOjplY291bnQoZ0QpCmBgYAoKIyBDb21tb24gaUdyYXBoIGZ1bmN0aW9ucwpDYWxjdWxhdGUgc29tZSBub2RlIHByb3BlcnRpZXMgYW5kIG5vZGUgc2ltaWxhcml0aWVzIHRoYXQgd2lsbCBiZSB1c2VkIHRvIGlsbHVzdHJhdGUgCmRpZmZlcmVudCBwbG90dGluZyBhYmlsaXRpZXMKCkNhbGN1bGF0ZSBkZWdyZWUgZm9yIGFsbCBub2RlcwpgYGB7cn0KZGVnQWxsIDwtIGlncmFwaDo6ZGVncmVlKGdELCB2ID0gaWdyYXBoOjpWKGdEKSwgbW9kZSA9ICJhbGwiKQpgYGAKCkNhbGN1bGF0ZSBiZXR3ZWVubmVzcyBmb3IgYWxsIG5vZGVzCmBgYHtyfQpiZXRBbGwgPC0gaWdyYXBoOjpiZXR3ZWVubmVzcyhnRCwgdiA9IGlncmFwaDo6VihnRCksIGRpcmVjdGVkID0gRkFMU0UpIC8gKCgoaWdyYXBoOjp2Y291bnQoZ0QpIC0gMSkgKiAoaWdyYXBoOjp2Y291bnQoZ0QpLTIpKSAvIDIpCmJldEFsbC5ub3JtIDwtIChiZXRBbGwgLSBtaW4oYmV0QWxsKSkvKG1heChiZXRBbGwpIC0gbWluKGJldEFsbCkpCnJtKGJldEFsbCkKYGBgCgpDYWxjdWxhdGUgRGljZSBzaW1pbGFyaXRpZXMgYmV0d2VlbiBhbGwgcGFpcnMgb2Ygbm9kZXMKYGBge3J9CmRzQWxsIDwtIGlncmFwaDo6c2ltaWxhcml0eS5kaWNlKGdELCB2aWRzID0gaWdyYXBoOjpWKGdEKSwgbW9kZSA9ICJhbGwiKQpgYGAKCiMgQWRkIGF0dHJpYnV0ZXMgdG8gbmV0d29yawpBZGQgbmV3IG5vZGUgYXR0cmlidXRlcyBiYXNlZCBvbiB0aGUgY2FsY3VsYXRlZCBub2RlIHByb3BlcnRpZXMvc2ltaWxhcml0aWVzCmBgYHtyfQpnRCA8LSBpZ3JhcGg6OnNldC52ZXJ0ZXguYXR0cmlidXRlKGdELCAiZGVncmVlIiwgaW5kZXggPSBpZ3JhcGg6OlYoZ0QpLCB2YWx1ZSA9IGRlZ0FsbCkKZ0QgPC0gaWdyYXBoOjpzZXQudmVydGV4LmF0dHJpYnV0ZShnRCwgImJldHdlZW5uZXNzIiwgaW5kZXggPSBpZ3JhcGg6OlYoZ0QpLCB2YWx1ZSA9IGJldEFsbC5ub3JtKQpgYGAKCkNoZWNrIHRoZSBhdHRyaWJ1dGVzLiBZb3Ugc2hvdWxkIHNlZSAiZGVncmVlIiBhbmQgImJldHdlZW5lc3MiIG5vdywgaW4gYWRkaXRpb24KdG8gIm5hbWUiLgpgYGB7cn0Kc3VtbWFyeShnRCkKYGBgCgpBbmQgbm93IGZvciB0aGUgZWRnZSBhdHRyaWJ1dGVzLi4uCmBgYHtyfQpGMSA8LSBmdW5jdGlvbih4KSB7ZGF0YS5mcmFtZShWNCA9IGRzQWxsW3doaWNoKGlncmFwaDo6VihnRCkkbmFtZSA9PSBhcy5jaGFyYWN0ZXIoeCRWMSkpLCB3aGljaChpZ3JhcGg6OlYoZ0QpJG5hbWUgPT0gYXMuY2hhcmFjdGVyKHgkVjIpKV0pfQpkYXRhU2V0LmV4dCA8LSBwbHlyOjpkZHBseShkYXRhU2V0LCAudmFyaWFibGVzPWMoIlYxIiwgIlYyIiwgIlYzIiksIGZ1bmN0aW9uKHgpIGRhdGEuZnJhbWUoRjEoeCkpKQoKZ0QgPC0gaWdyYXBoOjpzZXQuZWRnZS5hdHRyaWJ1dGUoZ0QsICJ3ZWlnaHQiLCBpbmRleCA9IGlncmFwaDo6RShnRCksIHZhbHVlID0gMCkKZ0QgPC0gaWdyYXBoOjpzZXQuZWRnZS5hdHRyaWJ1dGUoZ0QsICJzaW1pbGFyaXR5IiwgaW5kZXggPSBpZ3JhcGg6OkUoZ0QpLCB2YWx1ZSA9IDApCmBgYAoKKk5vdGU6IFRoZSBvcmRlciBvZiBpbnRlcmFjdGlvbnMgaW4gZGF0YVNldC5leHQgaXMgbm90IHRoZSBzYW1lIGFzIGl0IGlzIGluIGRhdGFTZXQgb3IgCmFzIGl0IGlzIGluIHRoZSBlZGdlIGxpc3QgYW5kIGZvciB0aGF0IHJlYXNvbiB0aGVzZSB2YWx1ZXMgY2Fubm90IGJlIGFzc2lnbmVkIGRpcmVjdGx5KgoKYGBge3J9CmZvciAoaSBpbiAxOm5yb3coZGF0YVNldC5leHQpKQp7CiAgICBpZ3JhcGg6OkUoZ0QpW2FzLmNoYXJhY3RlcihkYXRhU2V0LmV4dCRWMSkgJS0tJSBhcy5jaGFyYWN0ZXIoZGF0YVNldC5leHQkVjIpXSR3ZWlnaHQgPC0gYXMubnVtZXJpYyhkYXRhU2V0LmV4dCRWMykKICAgIGlncmFwaDo6RShnRClbYXMuY2hhcmFjdGVyKGRhdGFTZXQuZXh0JFYxKSAlLS0lIGFzLmNoYXJhY3RlcihkYXRhU2V0LmV4dCRWMildJHNpbWlsYXJpdHkgPC0gYXMubnVtZXJpYyhkYXRhU2V0LmV4dCRWNCkKfQpybShkYXRhU2V0LGRzQWxsLCBpLCBGMSkKYGBgCgpDaGVjayB0aGUgZWRnZSBhdHRyaWJ1dGVzLiBZb3Ugc2hvdWxkIHNlZSAid2VpZ2h0IiBhbmQgInNpbWlsYXJpdHkiIGFkZGVkLgpgYGB7cn0Kc3VtbWFyeShnRCkKYGBgCgojIExldHMgY2hlY2sgaXQgb3V0IGluIEN5dG9zY2FwZQoKKlVwZGF0ZTogWW91IGNhbiBnbyBzdHJhaWdodCBmcm9tIGlncmFwaCB0byBDeXRvc2NhcGUsIHNlbmRpbmcgYWxsIGF0dHJpYnV0ZXMgYW5kIGRpc3BsYXlpbmcgZ3JhcGghKgpgYGB7cn0KY3JlYXRlTmV0d29ya0Zyb21JZ3JhcGgoZ0QsbmV3LnRpdGxlPSdMZXMgTWlzZXJhYmxlcycpCmBgYAoKIyMgTGV0J3MgZGVjaWRlIG9uIGEgbGF5b3V0CkEgbGlzdCAgb2YgYXZhaWxhYmxlIGxheW91dHMgY2FuIGJlIGFjY2Vzc2VkIGZyb20gUiBhcyBmb2xsb3dzOgpgYGB7cn0KZ2V0TGF5b3V0TmFtZXMoKQpgYGAKCldlJ2xsIHNlbGVjdCB0aGUgImZydWNodGVybWFuLXJoZWluZ29sZCIgbGF5b3V0LiBUbyBzZWUgcHJvcGVydGllcyBmb3IgdGhlIGdpdmVuIGxheW91dCwgdXNlOgpgYGB7cn0KZ2V0TGF5b3V0UHJvcGVydHlOYW1lcygiZnJ1Y2h0ZXJtYW4tcmhlaW5nb2xkIikgCmBgYAoKV2UgY2FuIGNob29zZSBhbnkgcHJvcGVydHkgd2Ugd2FudCBhbmQgcHJvdmlkZSB0aGVtIGFzIGEgc3BhY2UtZGVsaW1pdGVkIHN0cmluZzoKYGBge3J9CmxheW91dE5ldHdvcmsoJ2ZydWNodGVybWFuLXJoZWluZ29sZCBncmF2aXR5X211bHRpcGxpZXI9MSBuSXRlcmF0aW9ucz0xMCcpCmBgYAoKQnV0IHRoYXQgaXMgYSBjcmF6eSBsYXlvdXQsIHNvIGxldCdzIHRyeSAiZm9yY2UtZGlyZWN0ZWQiIGluc3RlYWQ6CmBgYHtyfQpsYXlvdXROZXR3b3JrKCdmb3JjZS1kaXJlY3RlZCBkZWZhdWx0U3ByaW5nTGVuZ3RoPTcwIGRlZmF1bHRTcHJpbmdDb2VmZmljaWVudD0wLjAwMDAwMycpCmBgYAoKIyMgTmV4dCwgd2UgY2FuIHZpc3VhbGl6ZSBvdXIgZGF0YQoKT24gbm9kZXMuLi4KYGBge3J9CnNldE5vZGVDb2xvck1hcHBpbmcoJ2RlZ3JlZScsIGMobWluKGRlZ0FsbCksIG1lYW4oZGVnQWxsKSwgbWF4KGRlZ0FsbCkpLCBjKCcjRjVFREREJywgJyNGNTk3NzcnLCAnI0Y1NTMzMycpKQpsb2NrTm9kZURpbWVuc2lvbnMoVFJVRSkKc2V0Tm9kZVNpemVNYXBwaW5nKCdiZXR3ZWVubmVzcycsIGMobWluKGJldEFsbC5ub3JtKSwgbWVhbihiZXRBbGwubm9ybSksIG1heChiZXRBbGwubm9ybSkpLCBjKDMwLCA2MCwgMTAwKSkKYGBgCgouLi5hbmQgZWRnZXMKYGBge3J9CnNldEVkZ2VMaW5lV2lkdGhNYXBwaW5nKCd3ZWlnaHQnLCBjKG1pbihhcy5udW1lcmljKGRhdGFTZXQuZXh0JFYzKSksIG1lYW4oYXMubnVtZXJpYyhkYXRhU2V0LmV4dCRWMykpLCBtYXgoYXMubnVtZXJpYyhkYXRhU2V0LmV4dCRWMykpKSwgYygxLDMsNSkpCnNldEVkZ2VDb2xvck1hcHBpbmcoJ3dlaWdodCcsIGMobWluKGFzLm51bWVyaWMoZGF0YVNldC5leHQkVjMpKSwgbWVhbihhcy5udW1lcmljKGRhdGFTZXQuZXh0JFYzKSksIG1heChhcy5udW1lcmljKGRhdGFTZXQuZXh0JFYzKSkpLCBjKCcjQkJFRTAwJywgJyM3N0FBMDAnLCAnIzU1ODgwMCcpKQpgYGAKCldlIHdpbGwgZGVmaW5lIG91ciBvd24gZGVmYXVsdCBjb2xvci9zaXplIHNjaGVtYSBhZnRlciB3ZSBkZWZpbmVkIG5vZGUgYW5kIGVkZ2UgcnVsZXMsIGR1ZSB0byBwb3NzaWJsZSBpc3N1ZXMgd2hlbiB1c2luZyBydWxlcwpgYGB7cn0Kc2V0QmFja2dyb3VuZENvbG9yRGVmYXVsdCgnI0QzRDNEMycpCnNldE5vZGVCb3JkZXJDb2xvckRlZmF1bHQoJyMwMDAwMDAnKQpzZXROb2RlQm9yZGVyV2lkdGhEZWZhdWx0KDMpCnNldE5vZGVTaGFwZURlZmF1bHQoJ2VsbGlwc2UnKQpzZXROb2RlRm9udFNpemVEZWZhdWx0KDIwKQpzZXROb2RlTGFiZWxDb2xvckRlZmF1bHQoJyMwMDAwMDAnKQpgYGAKCjxjZW50ZXI+CiFbXShodHRwczovL2N5dG9zY2FwZS5naXRodWIuaW8vY3l0b3NjYXBlLWF1dG9tYXRpb24vZm9yLXNjcmlwdGVycy9SL25vdGVib29rcy9kYXRhL2ltZy9sZW1pcy5wbmcpe3dpZHRoPTYwJX0KPC9jZW50ZXI+CgpWb2lsYSEgQWxsIGRvbmUuCgojIFRyYWNrIHZlcnNpb25zIGZvciB5b3VyIHJlY29yZHMKYGBge3J9CmN5dG9zY2FwZVZlcnNpb25JbmZvKCkKYGBgCgpgYGB7cn0Kc2Vzc2lvbkluZm8oKQpgYGAK