--- title: "Getting started with *PathwaySpace* projection methods" author: "Sysbiolab Team" date: "`r Sys.Date()`" bibliography: bibliography.bib output: html_document: theme: cerulean self_contained: yes toc: true toc_float: true toc_depth: 2 css: custom.css vignette: > %\VignetteIndexEntry{"PathwaySpace: signals along geodesic paths"} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE, purl=FALSE} knitr::opts_chunk$set(echo = TRUE) ``` **Package**: PathwaySpace `r packageVersion('PathwaySpace')` # Highlights * Produces landscape images representing graphs by geodesic paths * Projects signals using a decay function to model signal attenuation * Applies a convolution algorithm to combine signals from neighboring vertices # Overview For a given *igraph* object containing vertices, edges, and a signal associated with the vertices, *PathwaySpace* performs a convolution operation, which involves a weighted combination of neighboring signals on a graph. **Figure 1A** illustrates the convolution operation problem. Each vertex's signal is positioned on a grid at specific `x` and `y` coordinates, represented by cones (for available signals) or question marks (for null or missing values). ```{r fig1, echo=FALSE, fig.cap="**Figure 1.** Signal processing addressed by the *PathwaySpace* package. **A**) Graph overlaid on a 2D coordinate system. Each projection cone represents the signal associated with a graph vertex (referred to as *vertex-signal positions*), while question marks indicate positions with no signal information (referred to as *null-signal positions*). **Inset**: Graph layout of the toy example used in the *quick start* section of this vignette. **B**) Illustration of signal projection from two neighboring vertices, simplified to one dimension. **Right**: Signal profiles from aggregation and decay functions.", out.width = '100%', purl=FALSE} knitr::include_graphics("figures/fig1.png") ```
Our model considers the vertex-signal positions as source points (or transmitters) and the null-signal positions as end points (or receivers). The signal values from vertex-signal positions are then projected to the null-signal positions according to a decay function, which will control how the signal values attenuate as they propagate across the 2D space. For a given null-signal position, the k-top signals are used to define the contributing vertices for the convolution operation, which will aggregate the signals from these contributing vertices considering their intensities reaching the end points. Users can adjust both the aggregation and decay functions; the aggregation function can be any arithmetic rule that reduces a numeric vector into a single scalar value (*e.g.*, mean, weighted mean), while available decay functions include linear, exponential, and Weibull models (**Fig.1B**). Additionally, users can assign vertex-specific decay functions to model signal projections for subsets of vertices that may exhibit distinct behaviors. The resulting image forms geodesic paths in which the signal has been projected from vertex- to null-signal positions, using a density metric to measure the signal intensity along these paths. # Setting basic input data ```{r Load packages - quick start, eval=TRUE, message=FALSE} #--- Load required packages for this section library(igraph) library(ggplot2) library(RGraphSpace) library(PathwaySpace) ``` This section will create an *igraph* object containing a binary signal associated to each vertex. The graph layout is configured manually to ensure that users can easily view all the relevant arguments needed to prepare the input data for the *PathwaySpace* package. The *igraph*'s `make_star()` function creates a star-like graph and the `V()` function is used to set attributes for the vertices. The *PathwaySpace* package will require that all vertices have `x`, `y`, and `name` attributes. ```{r Making a toy igraph - 1, eval=TRUE, message=FALSE} # Make a 'toy' igraph object, either a directed or undirected graph gtoy1 <- make_star(5, mode="undirected") # Assign 'x' and 'y' coordinates to each vertex # ..this can be an arbitrary unit in (-Inf, +Inf) V(gtoy1)$x <- c(0, 2, -2, -4, -8) V(gtoy1)$y <- c(0, 0, 2, -4, 0) # Assign a 'name' to each vertex (here, from n1 to n5) V(gtoy1)$name <- paste0("n", 1:5) ``` # Checking graph validity Next, we will create a *GraphSpace-class* object using the `GraphSpace()` constructor. This function will check the validity of the *igraph* object. For this example `mar = 0.2`, which sets the outer margins of the graph. ```{r GraphSpace constructor - 1, eval=TRUE, message=FALSE} # Check graph validity g_space1 <- GraphSpace(gtoy1, mar = 0.2) ``` Our graph is now ready for the *PathwaySpace* package. We can check its layout using the `plotGraphSpace()` function. ```{r GraphSpace constructor - 2, eval=FALSE, message=FALSE, out.width="100%"} # Check the graph layout plotGraphSpace(g_space1, add.labels = TRUE) ``` ```{r fig2.png, eval=FALSE, message=FALSE, echo=FALSE, include=FALSE, purl=FALSE} # gg <- plotGraphSpace(g_space1, add.labels = TRUE) # ggsave(filename = "./figures/fig2.png", height=4, width=5, # units="in", device="png", dpi=250, plot=gg) ``` ```{r fig2, echo=FALSE, out.width = '70%', purl=FALSE} knitr::include_graphics("figures/fig2.png") ``` # Creating a *PathwaySpace* object Next, we will create a *PathwaySpace-class* object using the `buildPathwaySpace()` constructor. This will calculate pairwise distances between vertices, subsequently required by the signal projection methods. ```{r PathwaySpace constructor - 1, eval=TRUE, message=FALSE} # Run the PathwaySpace constructor p_space1 <- buildPathwaySpace(g_space1) ``` As a default behavior, the `buildPathwaySpace()` constructor initializes the signal of each vertex as `0`. We can use the `vertexSignal()` accessor to get and set vertex signals in a *PathwaySpace* object; for example, in order to get vertex names and signal values: ```{r PathwaySpace constructor - 2, eval=TRUE, message=FALSE, results='hide'} # Check the number of vertices in the PathwaySpace object length(p_space1) ## [1] 5 # Check vertex names names(p_space1) ## [1] "n1" "n2" "n3" "n4" "n5" # Check signal (initialized with '0') vertexSignal(p_space1) ## n1 n2 n3 n4 n5 ## 0 0 0 0 0 ``` ...and for setting new signal values in *PathwaySpace* objects: ```{r PathwaySpace constructor - 3, eval=TRUE, message=FALSE, results='hide'} # Set new signal to all vertices vertexSignal(p_space1) <- c(1, 4, 2, 4, 3) # Set a new signal to the 1st vertex vertexSignal(p_space1)[1] <- 2 # Set a new signal to vertex "n1" vertexSignal(p_space1)["n1"] <- 6 # Check updated signal values vertexSignal(p_space1) ## n1 n2 n3 n4 n5 ## 6 4 2 4 3 ``` # Signal projection ## Circular projection Following that, we will use the `circularProjection()` function to project the network signals by the `weibullDecay()` function with `pdist = 0.4`, which is passed by the `decay.fun` argument. This term determines a distance unit for the signal convolution, affecting the extent over which the convolution operation projects the signal. For example, when `pdist = 1`, it will represent the diameter of the inscribed circle within the coordinate space. We also set `k = 1`, which defines the contributing vertices for signal convolution. ```{r Circular projection - 1, eval=FALSE, message=FALSE, out.width="70%"} # Run signal projection p_space1 <- circularProjection(p_space1, k = 1, decay.fun = weibullDecay(pdist = 0.4)) # Plot a PathwaySpace image plotPathwaySpace(p_space1, add.marks = TRUE) ``` ```{r fig3.png, eval=FALSE, message=FALSE, echo=FALSE, include=FALSE, purl=FALSE} # gg <- plotPathwaySpace(p_space1, add.marks = TRUE) # ggsave(filename = "./figures/fig3.png", height=3.5, width=5, # units="in", device="png", dpi=350, plot=gg) ``` ```{r fig3, echo=FALSE, out.width = '75%', purl=FALSE} knitr::include_graphics("figures/fig3.png") ``` Next, we reassess the same *PathwaySpace* object, using `pdist = 0.2`, `k = 2` and adjusting the `shape` of the decay function (for further details, see the [**online tutorials**](https://sysbiolab.github.io/PathwaySpace/)). ```{r Circular projection - 3, eval=FALSE, message=FALSE, out.width="70%"} # Re-run signal projection, adjusting Weibull's shape p_space1 <- circularProjection(p_space1, k = 2, decay.fun = weibullDecay(shape = 2, pdist = 0.2)) # Plot PathwaySpace plotPathwaySpace(p_space1, marks = "n1", theme = "th2") ``` ```{r fig4.png, eval=FALSE, message=FALSE, echo=FALSE, include=FALSE, purl=FALSE} # gg <- plotPathwaySpace(p_space1, marks = "n1", theme = "th2") # ggsave(filename = "./figures/fig4.png", height=3.5, width=5, # units="in", device="png", dpi=350, plot=gg) ``` ```{r fig4, echo=FALSE, out.width = '75%', purl=FALSE} knitr::include_graphics("figures/fig4.png") ``` The `shape` parameter allows a projection to take a variety of shapes. When `shape = 1` the projection follows an exponential decay, and when `shape > 1` the projection is first convex, then concave with an inflection point along the decay path. For additional examples see the [**modeling signal decay**](https://sysbiolab.github.io/PathwaySpace/modeling-signal-decay.html) tutorial. ## Polar projection In this section we will project network signals using a polar coordinate system. This representation may be useful for certain types of data, for example, to highlight patterns of signal propagation on directed graphs, especially to explore the orientation aspect of signal flow. To demonstrate this feature we will used the `gtoy2` directed graph, available in the *RGraphSpace* package. ```{r Polar projection - 1, eval=TRUE, message=FALSE, out.width="100%"} # Load a pre-processed directed igraph object data("gtoy2", package = "RGraphSpace") # Check graph validity g_space2 <- GraphSpace(gtoy2, mar = 0.2) ``` ```{r Polar projection - 2, eval=FALSE, message=FALSE, out.width="100%"} # Check the graph layout plotGraphSpace(g_space2, add.labels = TRUE) ``` ```{r fig5.png, eval=FALSE, message=FALSE, echo=FALSE, include=FALSE, purl=FALSE} # gg <- plotGraphSpace(g_space2, add.labels = TRUE) # ggsave(filename = "./figures/fig5.png", height=4, width=5, # units="in", device="png", dpi=250, plot=gg) ``` ```{r fig5, echo=FALSE, out.width = '75%', purl=FALSE} knitr::include_graphics("figures/fig5.png") ``` ```{r Polar projection - 3, eval=TRUE, message=FALSE} # Build a PathwaySpace for the 'g_space2' p_space2 <- buildPathwaySpace(g_space2) # Set '1s' as vertex signal vertexSignal(p_space2) <- 1 ``` For fine-grained modeling of signal decay, the `vertexDecay()` accessor allows assigning decay functions at the level of individual vertices. For example, adjusting Weibull's `shape` argument for node `n6`: ```{r Polar projection - 4, eval=TRUE, message=FALSE} # Modify decay function # ..for all vertices vertexDecay(p_space2) <- weibullDecay(shape=2, pdist = 1) # ..for individual vertices vertexDecay(p_space2)[["n6"]] <- weibullDecay(shape=3, pdist = 1) ``` In polar projections, the `pdist` term defines a reference distance related to edge length, aiming to constrain signal projections within edge bounds. Here we set `pdist = 1` to reach full edge lengths. Next, we run the signal projection using polar coordinates. The `beta` exponent will control the angular span; for values greater than zero, `beta` will progressively narrow the projection along the edge axis. ```{r Polar projection - 5, eval=FALSE, message=FALSE, out.width="70%"} # Run signal projection using polar coordinates p_space2 <- polarProjection(p_space2, beta = 10) # Plot PathwaySpace plotPathwaySpace(p_space2, theme = "th2", add.marks = TRUE) ``` ```{r fig6.png, eval=FALSE, message=FALSE, echo=FALSE, include=FALSE, purl=FALSE} # gg <- plotPathwaySpace(p_space2, theme = "th2", add.marks = TRUE) # ggsave(filename = "./figures/fig6.png", height=3.5, width=5, # units="in", device="png", dpi=350, plot=gg) ``` ```{r fig6, echo=FALSE, out.width = '75%', purl=FALSE} knitr::include_graphics("figures/fig6.png") ``` Note that this projection distributes signals on the edges regardless of direction. To incorporate edge orientation, we set `directional = TRUE`, which channels the projection along the paths: ```{r Polar projection - 6, eval=FALSE, message=FALSE, out.width="70%"} # Re-run signal projection using 'directional = TRUE' p_space2 <- polarProjection(p_space2, beta = 10, directional = TRUE) # Plot PathwaySpace plotPathwaySpace(p_space2, theme = "th2", marks = c("n1","n3","n4","n5")) ``` ```{r fig7.png, eval=FALSE, message=FALSE, echo=FALSE, include=FALSE, purl=FALSE} # gg <- plotPathwaySpace(p_space2, theme = "th2", marks = c("n1","n3","n4","n5")) # ggsave(filename = "./figures/fig7.png", height=3.5, width=5, # units="in", device="png", dpi=350, plot=gg) ``` ```{r fig7, echo=FALSE, out.width = '75%', purl=FALSE} knitr::include_graphics("figures/fig7.png") ``` This *PathwaySpace* polar projection emphasizes the signal flow along the directional pattern of a directed graph (see the *igraph* plot above). When interpreting, users should note that this approach introduces simplifications; for example, depending on the network topology, the polar projection may fail to capture complex features of directed graphs, such as cyclic dependencies, feedforward and feedback loops, or other intricate interactions. # Signal types The *PathwaySpace* accepts binary, integer, and numeric signal types, including `NAs`. If a vertex signal is assigned with `NA`, it will be ignored by the convolution algorithm. Logical values are also allowed, but it will be treated as binary. Next, we show the projection of a signal that includes negative values, using the `p_space1` object created previously. ```{r Signal types, eval=FALSE, message=FALSE, out.width="70%"} # Set a negative signal to vertices "n3" and "n4" vertexSignal(p_space1)[c("n3","n4")] <- c(-2, -4) # Check updated signal vector vertexSignal(p_space1) # n1 n2 n3 n4 n5 # 6 4 -2 -4 3 # Re-run signal projection p_space1 <- circularProjection(p_space1, decay.fun = weibullDecay(shape = 2)) # Plot PathwaySpace plotPathwaySpace(p_space1, bg.color = "white", font.color = "grey20", add.marks = TRUE, mark.color = "magenta", theme = "th2") ``` ```{r fig8.png, eval=FALSE, message=FALSE, echo=FALSE, include=FALSE, purl=FALSE} # gg <- plotPathwaySpace(p_space1, bg.color = "white", font.color = "grey20", # add.marks = TRUE, mark.color = "magenta", theme = "th2") # ggsave(filename = "./figures/fig8.png", height=3.5, width=5, # units="in", device="png", dpi=350, plot=gg) ``` ```{r fig8, echo=FALSE, out.width = '75%', purl=FALSE} knitr::include_graphics("figures/fig8.png") ``` Note that the original signal vector was rescale to `[-1, +1]`. If the signal vector is `>=0`, then it will be rescaled to `[0, 1]`; if the signal vector is `<=0`, it will be rescaled to `[-1, 0]`; and if the signal vector is in `(-Inf, +Inf)`, then it will be rescaled to `[-1, +1]`. To override this signal processing, simply set `rescale = FALSE` in the projection function. # Online tutorials https://sysbiolab.github.io/PathwaySpace/ # Citation If you use *PathwaySpace*, please cite: * Tercan & Apolonio et al. Protocol for assessing distances in pathway space for classifier feature sets from machine learning methods. *STAR Protocols* 6(2):103681, 2025. https://doi.org/10.1016/j.xpro.2025.103681 * Ellrott et al. Classification of non-TCGA cancer samples to TCGA molecular subtypes using compact feature sets. *Cancer Cell* 43(2):195-212.e11, 2025. https://doi.org/10.1016/j.ccell.2024.12.002 # Other useful links * RGraphSpace: A Lightweight Interface Between 'ggplot2' and 'igraph' Objects https://cran.r-project.org/package=RGraphSpace * SpotSpace: Methods for Projecting Network Signals in Spatial Transcriptomics https://github.com/sysbiolab/SpotSpace * RedeR: Interactive visualization and manipulation of nested networks https://bioconductor.org/packages/RedeR/ # Session information ```{r label='Session information', eval=TRUE, echo=FALSE} sessionInfo() ```