threadleR

Overview

threadleR is an R client for Threadle, a high-performance network engine. It starts a background Threadle process, sends commands, and returns results back to R. Most functions are thin wrappers around Threadle CLI commands.

Installation

Install from GitHub:

# install.packages("remotes")
remotes::install_github("YukunJiao/threadleR")

Load the package:

library(threadleR)

Prerequisites

Check whether Threadle is available

th_is_available() checks whether the threadle executable can be found on your system.

th_is_available()

If it returns FALSE, either add threadle to your PATH or start Threadle with an explicit path:

th_start_threadle("/full/path/to/threadle")

Start Threadle

Start the Threadle background process:

th_start_threadle()

Stop Threadle

Stop the Threadle process when you are done:

th_stop_threadle()
#> Threadle process terminated.

Working directory

Threadle has its own working directory. Many file-based commands (especially th_load_file()) depend on it.

To inspect Threadle working directory:

th_get_workdir()

To set it:

th_set_workdir("~documents")   # or "~", or an absolute path

To keep Threadle aligned with the current R working directory:

th_sync_wd()

Example data: Lazega

Threadle network files may reference companion files (e.g., a nodeset file). When you load a network from disk, Threadle will also try to load the referenced nodeset file using its current working directory.

Stage example files to your working directory

th_start_threadle()

th_sync_wd()
#> Set current working directory to '/Users/doge/Documents/threadleR/vignettes'.
#> Threadle working directory synced to: /Users/doge/Documents/threadleR/vignettes
exdir <- th_stage_examples_to_wd(folder = "threadle_examples", overwrite = TRUE)

th_set_workdir(exdir)
#> Set current working directory to '/Users/doge/Documents/threadleR/vignettes/threadle_examples'.

list.files(exdir)
#> [1] "dscw_nodeset.tsv"          "dscw.tsv"                 
#> [3] "lazega_female_nodeset.tsv" "lazega_female.tsv"        
#> [5] "lazega_nodes.tsv"          "lazega.tsv"               
#> [7] "mynet_nodesetfile.tsv"     "mynet.tsv"                
#> [9] "mynodes.tsv"

Load Lazega network (and its referenced nodeset)

net_file <- file.path(exdir, "lazega.tsv")

lazega <- th_load_file("lazega", net_file, type = "network")
#> Set current working directory to '/Users/doge/Documents/threadleR/vignettes/threadle_examples'.
#> Loaded structure 'lazega' from 'lazega.tsv'
#> Set current working directory to '/Users/doge/Documents/threadleR/vignettes/threadle_examples'.

# th_load_file("lazega", ..., type = "network") also creates:
# lazega_nodeset  (a threadle_nodeset handle in the calling environment)
th_i()
#> Inventory contains 2 structure(s)
#> $lazega
#> [1] "Network"
#> 
#> $lazega_nodeset
#> [1] "Nodeset"
th_info(lazega)
#> Returning metadata about 'lazega'
#> $Type
#> [1] "Network"
#> 
#> $Name
#> [1] "lazega"
#> 
#> $Filepath
#> [1] "lazega.tsv"
#> 
#> $isModified
#> [1] FALSE
#> 
#> $Nodeset
#> [1] "Lawyers"
#> 
#> $Layers
#>            Name Mode Directionality ValueType SelftiesAllowed NbrEdges
#> 1       friends    1       Directed    Binary           FALSE      575
#> 2        advice    1       Directed    Binary           FALSE      892
#> 3 collaboration    1     Undirected    Binary           FALSE      378
th_info(lazega_nodeset)
#> Returning metadata about 'Lawyers'
#> $Type
#> [1] "Nodeset"
#> 
#> $Name
#> [1] "Lawyers"
#> 
#> $Filepath
#> [1] "lazega_nodes.tsv"
#> 
#> $isModified
#> [1] FALSE
#> 
#> $NbrNodes
#> [1] 71
#> 
#> $NodeAttributes
#>            Name Type
#> 1        Status Char
#> 2        Gender Char
#> 3        Office Char
#> 4 YearsWithFirm  Int
#> 5           Age  Int
#> 6      Practice Char
#> 7     LawSchool Char

Preview and inventory

th_preview(lazega, maxlines = 20)
#> Preview of IStructure 'lazega'
#>  [1] "Network: lazega"                                                 
#>  [2] "Nodeset: Lawyers"                                                
#>  [3] " friends [1-mode: Binary,Directed,False); Nbr edges:575]"        
#>  [4] "1 -> 2"                                                          
#>  [5] "1 -> 4"                                                          
#>  [6] "1 -> 8"                                                          
#>  [7] "1 -> 17"                                                         
#>  [8] "2 -> 16"                                                         
#>  [9] "2 -> 17"                                                         
#> [10] "2 -> 22"                                                         
#> [11] "2 -> 26"                                                         
#> [12] "4 -> 2"                                                          
#> [13] "4 -> 3"                                                          
#> [14] " advice [1-mode: Binary,Directed,False); Nbr edges:892]"         
#> [15] "1 -> 2"                                                          
#> [16] "1 -> 17"                                                         
#> [17] "1 -> 20"                                                         
#> [18] "2 -> 1"                                                          
#> [19] "2 -> 6"                                                          
#> [20] "2 -> 17"                                                         
#> [21] "2 -> 20"                                                         
#> [22] "2 -> 22"                                                         
#> [23] "2 -> 24"                                                         
#> [24] "2 -> 26"                                                         
#> [25] " collaboration [1-mode: Binary,Undirected,False); Nbr edges:378]"
#> [26] "1 <-> 17"                                                        
#> [27] "1 <-> 39"                                                        
#> [28] "1 <-> 40"                                                        
#> [29] "1 <-> 41"                                                        
#> [30] "17 <-> 1"                                                        
#> [31] "17 <-> 19"                                                       
#> [32] "17 <-> 22"                                                       
#> [33] "17 <-> 24"                                                       
#> [34] "17 <-> 25"                                                       
#> [35] "17 <-> 26"
th_i()
#> Inventory contains 2 structure(s)
#> $lazega
#> [1] "Network"
#> 
#> $lazega_nodeset
#> [1] "Nodeset"

Adding and removing ties

One-mode layer: edges

Create a layer and add a few edges:

th_add_layer(lazega, "friends2", mode = 1, directed = FALSE, valuetype = "binary")
#> Layer 'friends2' added to network 'lazega'

th_add_edge(lazega, "friends2", node1id = 1, node2id = 2)
#> Added edge between 1 and 2 (value=1) in layer 'friends2'.
th_add_edge(lazega, "friends2", node1id = 2, node2id = 3)
#> Added edge between 2 and 3 (value=1) in layer 'friends2'.

th_check_edge(lazega, "friends2", node1id = 1, node2id = 2)
#> [1] TRUE
th_get_all_edges(lazega, "friends2", offset = 0, limit = 10)
#> Returning all 2 edge(s) in layer 'friends2':
#>   node1 node2 value
#> 1     1     2     1
#> 2     2     3     1

Remove an edge or clear a layer:

th_remove_edge(lazega, "friends2", node1id = 1, node2id = 2)
#> Removed edge between 1 and 2 in layer 'friends2'.
th_clear_layer(lazega, "friends2")
#> All edges removed from layer 'friends2' in network 'lazega'.

Two-mode layer: affiliations (hyperedges)

th_add_layer(lazega, "clubs", mode = 2)
#> Layer 'clubs' added to network 'lazega'

th_add_hyper(lazega, "clubs", hypername = "group1", nodes = c(1, 2, 3))
#> Added hyperedge 'group1' (with 3 nodes) in layer 'clubs'.
th_get_node_hyperedges(lazega, "clubs", nodeid = 1)
#> Node '1' is affiliated to the following hyperedges in layer 'clubs':
#> [1] "group1"
th_get_hyperedge_nodes(lazega, "clubs", hypername = "group1")
#> Hyperedge 'group1' connects the following nodes in layer 'clubs':
#> [1] 1 2 3

th_add_aff(lazega, "clubs", nodeid = 4, hypername = "group1")
#> Node '4' affiliated to hyperedge 'group1' in 2-mode layer 'clubs'.
th_remove_aff(lazega, "clubs", nodeid = 4, hypername = "group1")
#> Node '4' no longer affiliated to hyperedge 'group1' in 2-mode layer 'clubs'.

th_get_all_hyperedges(lazega, "clubs", offset = 0, limit = 10)
#> Returning all 1 hyperedge(s) in layer 'clubs':
#> [1] "group1"

Nodes and attributes

Add nodes

Add a node to the nodeset:

th_add_node(lazega_nodeset, nodeid = 999)
#> Node ID '999' added to nodeset 'Lawyers'.
th_get_nbr_nodes(lazega_nodeset)
#> Number of nodes in 'Lawyers': 72
#> [1] 72

Define and query attributes

Define an integer attribute and set values:

th_define_attr(lazega_nodeset, "score", "int")
#> Node attribute 'score' of attrType Int defined.
th_set_attr(lazega_nodeset, nodeid = 1, attrname = "score", attrvalue = 10)
#> Attribute 'score' for node 1 set to 10.
th_get_attr(lazega_nodeset, nodeid = 1, attrname = "score")
#> [1] 10
th_get_attr_summary(lazega_nodeset, "score")
#> $AttributeName
#> [1] "score"
#> 
#> $AttributeType
#> [1] "Int"
#> 
#> $Statistics
#> $Statistics$Mean
#> [1] 10
#> 
#> $Statistics$Median
#> [1] 10
#> 
#> $Statistics$StdDev
#> [1] 0
#> 
#> $Statistics$Min
#> [1] 10
#> 
#> $Statistics$Max
#> [1] 10
#> 
#> $Statistics$Q1
#> [1] 10
#> 
#> $Statistics$Q3
#> [1] 10
#> 
#> $Statistics$Count
#> [1] 1
#> 
#> $Statistics$Missing
#> [1] 71
#> 
#> $Statistics$PercentageWithValue
#> [1] 1.388889

Remove values or undefine the attribute:

th_remove_attr(lazega_nodeset, nodeid = 1, attrname = "score")
#> Attribute 'score' removed for node 1.
th_undefine_attr(lazega_nodeset, "score")
#> Node attribute 'score' is no longer defined.

Generate random attributes:

th_generate_attr(lazega_nodeset, "x", attrtype = "int", min = 1, max = 10)
#> Node attribute 'x' (integer) defined and random values between 1 and 10 assigned to all nodes.
th_get_attr_summary(lazega_nodeset, "x")
#> $AttributeName
#> [1] "x"
#> 
#> $AttributeType
#> [1] "Int"
#> 
#> $Statistics
#> $Statistics$Mean
#> [1] 5.416667
#> 
#> $Statistics$Median
#> [1] 6
#> 
#> $Statistics$StdDev
#> [1] 2.711857
#> 
#> $Statistics$Min
#> [1] 1
#> 
#> $Statistics$Max
#> [1] 10
#> 
#> $Statistics$Q1
#> [1] 3
#> 
#> $Statistics$Q3
#> [1] 8
#> 
#> $Statistics$Count
#> [1] 72
#> 
#> $Statistics$Missing
#> [1] 0
#> 
#> $Statistics$PercentageWithValue
#> [1] 100

Basic network routines

Compute density (for a one-mode layer):

th_density(lazega, "friends")
#> [1] 0.1124804

Degree (writes an attribute):

th_degree(lazega, "friends", attrname = "deg", direction = "both")
#> Node attribute 'deg' set for 72 nodes in nodeset 'Lawyers'.
th_get_attr(lazega, nodeid = 2, attrname = "deg")
#> [1] 14

Connected components (writes component IDs to an attribute):

th_components(lazega, "friends", attrname = "comp")
#> $NbrComponents
#> [1] 4
#> 
#> $ComponentSizes
#> [1] 69  1  1  1
th_get_attr_summary(lazega, "comp")
#> $AttributeName
#> [1] "comp"
#> 
#> $AttributeType
#> [1] "Int"
#> 
#> $Statistics
#> $Statistics$Mean
#> [1] 0.08333333
#> 
#> $Statistics$Median
#> [1] 0
#> 
#> $Statistics$StdDev
#> [1] 0.4330127
#> 
#> $Statistics$Min
#> [1] 0
#> 
#> $Statistics$Max
#> [1] 3
#> 
#> $Statistics$Q1
#> [1] 0
#> 
#> $Statistics$Q3
#> [1] 0
#> 
#> $Statistics$Count
#> [1] 72
#> 
#> $Statistics$Missing
#> [1] 0
#> 
#> $Statistics$PercentageWithValue
#> [1] 100

Shortest path:

th_shortest_path(lazega, node1id = 1, node2id = 3, layername = "friends")
#> Shortest path from node '1' to '3' is 2
#> [1] 2

Import and export layer data

Export a layer to an edge list:

out <- tempfile(fileext = ".tsv")
th_export_layer(lazega, "friends", file = out, header = TRUE, sep = "\t")
#> Exported layer 'friends' to file: /var/folders/ws/ns45p4ns4kd7jd3xk6t47h1w0000gn/T//RtmprstflM/file13fbee117edf.tsv
readLines(out, n = 3)
#> [1] "from\tto" "1\t2"     "1\t4"
unlink(out)

Import edges into an existing layer:

tmp <- tempfile(fileext = ".tsv")
write.table(
  data.frame(node1 = c(1, 2), node2 = c(2, 3), value = c(1, 1)),
  file = tmp, sep = "\t", row.names = FALSE, col.names = FALSE, quote = FALSE
)

th_import_layer(lazega, "friends",
  file = tmp, format = "edgelist",
  node1col = 0, node2col = 1, valuecol = 2,
  header = FALSE, sep = "\t"
)
#> Imported edgelist to 1-mode layer 'friends'

th_check_edge(lazega, "friends", node1id = 1, node2id = 2)
#> [1] TRUE
unlink(tmp)

Save and load Threadle structures

Save nodeset or network:

tmp_net <- tempfile(fileext = ".tsv")
th_save_file(lazega, file = tmp_net)
#> Saved network 'lazega' to file: /var/folders/ws/ns45p4ns4kd7jd3xk6t47h1w0000gn/T//RtmprstflM/file13fbe440398d6.tsv, and saved nodeset 'Lawyers' to file: lazega_nodes.tsv.
file.exists(tmp_net)
#> [1] TRUE
unlink(tmp_net)

Load back from file:

# Ensure Threadle working directory can resolve referenced files if loading a network
# (for a standalone roundtrip, prefer staging all required files into one folder first)

Cleanup

Delete individual structures:

th_delete(lazega)
#> Structure 'lazega' removed.
th_delete(lazega_nodeset)
#> Structure 'lazega_nodeset' removed.
th_i()
#> No structures stored in the current session.

Or delete everything:

th_delete_all()
#> All structures removed.
th_i()
#> No structures stored in the current session.

Stop Threadle:

th_stop_threadle()
#> Threadle process terminated.

if (exists("exdir", inherits = FALSE) && is.character(exdir) && nzchar(exdir) && dir.exists(exdir)) {
  unlink(exdir, recursive = TRUE, force = TRUE)
}

Session info

sessionInfo()
#> R version 4.5.2 (2025-10-31)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Sequoia 15.7.3
#> 
#> Matrix products: default
#> BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
#> 
#> locale:
#> [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#> 
#> time zone: Europe/Stockholm
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] threadleR_0.4.3
#> 
#> loaded via a namespace (and not attached):
#>  [1] digest_0.6.37     R6_2.6.1          fastmap_1.2.0     xfun_0.52        
#>  [5] cachem_1.1.0      knitr_1.50        htmltools_0.5.8.1 rmarkdown_2.29   
#>  [9] lifecycle_1.0.4   ps_1.9.1          cli_3.6.5         processx_3.8.6   
#> [13] sass_0.4.10       jquerylib_0.1.4   compiler_4.5.2    rstudioapi_0.17.1
#> [17] tools_4.5.2       evaluate_1.0.3    bslib_0.9.0       yaml_2.3.10      
#> [21] rlang_1.1.6       jsonlite_2.0.0