Generate more realistic graph, render to SVG

This commit is contained in:
Pascal Engélibert 2025-10-12 19:23:33 +02:00
commit f2798a14d4
7 changed files with 366 additions and 131 deletions

197
Cargo.lock generated
View file

@ -2,15 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "argp" name = "argp"
version = "0.4.0" version = "0.4.0"
@ -33,19 +24,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "assert_approx_eq" name = "autocfg"
version = "1.1.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "assert_unordered"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74323b7881323eb351134e08ee5331594826789557afef8e309baf481b2264"
dependencies = [
"ansi_term",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@ -53,6 +35,12 @@ version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.3" version = "1.0.3"
@ -84,18 +72,25 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "forceatlas2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c192796ef315da1fb02da466c03f2a16948d55fe1661925a002a9d1e41c571f"
dependencies = [
"bumpalo",
"num-traits",
"parking_lot",
"rand",
"rayon",
]
[[package]] [[package]]
name = "getopts" name = "getopts"
version = "0.2.24" version = "0.2.24"
@ -116,40 +111,21 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "graphrs"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f62f4b6f1f9ec5757abcebef6a96983056b54109d5346aa6691c67a0e39ef32"
dependencies = [
"assert_approx_eq",
"assert_unordered",
"doc-comment",
"itertools",
"nohash",
"quick-xml",
"rand",
"rand_chacha",
"rayon",
"serde",
"sorted-vec",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.177" version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.6" version = "2.7.6"
@ -157,10 +133,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]] [[package]]
name = "nohash" name = "num-traits"
version = "0.2.0" version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0f889fb66f7acdf83442c35775764b51fed3c606ab9cee51500dbde2cf528ca" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
@ -192,15 +194,6 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "quick-xml"
version = "0.37.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.41" version = "1.0.41"
@ -260,49 +253,35 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "reticulum-synthesis" name = "reticulum-synthesis"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"argp", "argp",
"graphrs", "forceatlas2",
"rand",
] ]
[[package]] [[package]]
name = "serde" name = "scopeguard"
version = "1.0.228" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]] [[package]]
name = "serde_core" name = "smallvec"
version = "1.0.228" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sorted-vec"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f58d7b0190c7f12df7e8be6b79767a0836059159811b869d5ab55721fe14d0"
[[package]] [[package]]
name = "syn" name = "syn"
@ -340,26 +319,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "winapi" name = "windows-link"
version = "0.3.9" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"

View file

@ -7,9 +7,16 @@ authors = ["tuxmain <tuxmain@zettascript.org>"]
repository = "https://git.txmn.tk/tuxmain/reticulum-synthesis" repository = "https://git.txmn.tk/tuxmain/reticulum-synthesis"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
[features]
default = ["render"]
# Render graph to SVG
render = ["dep:forceatlas2"]
[dependencies] [dependencies]
argp = "0.4.0" argp = "0.4.0"
graphrs = "0.11.13" forceatlas2 = { version = "0.8.0", optional = true }
rand = "0.8.5"
[profile.release] [profile.release]
lto = true lto = true

View file

@ -4,6 +4,8 @@ A simple program to generate and run random topologies of [Reticulum](https://re
The purpose is to test routing and applications easily. The purpose is to test routing and applications easily.
![Graph of 50 nodes, with some highly connected clusters and more isolated parts.](graph.svg)
You can contact `tuxmain` on the Reticulum Matrix channel for any request. You can contact `tuxmain` on the Reticulum Matrix channel for any request.
Development status: Early development, just for fun, to tinker with Reticulum and learn about mesh networks. Development status: Early development, just for fun, to tinker with Reticulum and learn about mesh networks.
@ -31,6 +33,10 @@ Option `--gen` (or `-g`) generates the network graph and creates the directories
Option `--run` (or `-r`) runs the generated nodes using `rnsd`. (tries to run for any subdirectory in the directory passed to `-d`) Option `--run` (or `-r`) runs the generated nodes using `rnsd`. (tries to run for any subdirectory in the directory passed to `-d`)
Tune the graph density with the `-p <float>` option that accepts an edge probability number between 0 and 1.
When generating a graph, option `-R <path>` renders the graph to an SVG image. (may take some time to compute, depending on the number of nodes)
The network can then be tested using RNS tools: (from reticulum repository) The network can then be tested using RNS tools: (from reticulum repository)
``` ```

11
graph.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

35
src/generator.rs Normal file
View file

@ -0,0 +1,35 @@
use rand::Rng;
fn get_neighbors(node: usize, edges: &[(usize, usize)]) -> impl Iterator<Item = usize> {
edges.iter().filter_map(move |(n1, n2)| {
if *n1 == node {
Some(*n2)
} else if *n2 == node {
Some(*n1)
} else {
None
}
})
}
/// Generate a random social graph
pub fn generate(n: usize, proba: f32, mut rng: impl Rng) -> Vec<(usize, usize)> {
let mut edges = Vec::new();
for friend in 1..n {
// Select a random anchor node in the graph.
let anchor = rng.gen_range(0..friend);
// The anchor is calling a friend to join the graph.
let mut new_friends = vec![anchor];
// Each other anchor's friend is also a friend of the new one with some probability.
for neighbor in get_neighbors(anchor, &edges) {
if rng.r#gen::<f32>() < proba {
new_friends.push(neighbor);
}
}
for new_friend in new_friends {
edges.push((new_friend, friend));
}
}
edges
}

View file

@ -1,3 +1,6 @@
mod generator;
mod render;
use std::{ use std::{
collections::{HashMap, hash_map::Entry}, collections::{HashMap, hash_map::Entry},
io::Write, io::Write,
@ -19,6 +22,10 @@ struct Cli {
/// Probability of edge /// Probability of edge
#[argp(option, short = 'p', default = "0.5")] #[argp(option, short = 'p', default = "0.5")]
proba: f32, proba: f32,
/// Output SVG image of the graph (only with --gen)
#[cfg(feature = "render")]
#[argp(option, short = 'R')]
render: Option<String>,
/// Start the Reticulum nodes /// Start the Reticulum nodes
#[argp(switch, short = 'r')] #[argp(switch, short = 'r')]
run: bool, run: bool,
@ -28,17 +35,22 @@ fn main() {
let cli: Cli = argp::parse_args_or_exit(argp::DEFAULT); let cli: Cli = argp::parse_args_or_exit(argp::DEFAULT);
if cli.generate { if cli.generate {
let graph = graphrs::generators::random::fast_gnp_random_graph( let mut rng = rand::thread_rng();
cli.nodes as _,
cli.proba as _, assert!(
false, cli.proba >= 0.0 && cli.proba <= 1.0,
None, "Probability should be between 0 and 1."
) );
.unwrap(); let edges = generator::generate(cli.nodes, cli.proba, &mut rng);
let mut ports = HashMap::new(); let mut ports = HashMap::new();
let mut port: u16 = 42000; let mut port: u16 = 42000;
assert!(graph.number_of_edges() < 65535 - port as usize); assert!(edges.len() < 65535 - port as usize);
#[cfg(feature = "render")]
if let Some(out_svg) = &cli.render {
render::render_to_svg(cli.nodes, &edges, out_svg, &mut rng);
}
let dir = PathBuf::from(&cli.dir); let dir = PathBuf::from(&cli.dir);
std::fs::DirBuilder::new() std::fs::DirBuilder::new()
@ -76,11 +88,17 @@ loglevel = 4
.as_bytes(), .as_bytes(),
) )
.unwrap(); .unwrap();
for edge in graph.get_edges_for_node(node as _).unwrap() { for edge in edges.iter().filter_map(|(n1, n2)| {
let key = if edge.u < edge.v { if *n1 == node || *n2 == node {
(edge.u, edge.v) Some((*n1, *n2))
} else { } else {
(edge.v, edge.u) None
}
}) {
let key = if edge.0 < edge.1 {
(edge.0, edge.1)
} else {
(edge.1, edge.0)
}; };
let mut edge_ports = (port, port + 1); let mut edge_ports = (port, port + 1);
match ports.entry(key) { match ports.entry(key) {
@ -103,7 +121,7 @@ listen_port = {}
forward_ip = 127.0.0.1 forward_ip = 127.0.0.1
forward_port = {} forward_port = {}
", ",
edge.u, edge.v, edge_ports.0, edge_ports.1 edge.0, edge.1, edge_ports.0, edge_ports.1
) )
.as_bytes(), .as_bytes(),
) )

195
src/render.rs Normal file
View file

@ -0,0 +1,195 @@
use forceatlas2::{AbstractNode, Layout};
use rand::Rng;
use std::io::Write;
pub fn render_to_svg(n: usize, edges: &[(usize, usize)], path: &str, mut rng: impl Rng) {
let width = 512;
let height = 512;
let node_radius = 12.0;
let mut layout = Layout::<f32, 2>::from_abstract(
forceatlas2::Settings {
theta: 0.5,
ka: 1.0,
kg: 1.0,
kr: 1.0,
lin_log: false,
prevent_overlapping: None,
speed: 0.1,
strong_gravity: false,
},
(0..n).map(|i| {
(
i,
AbstractNode {
size: 1.0,
mass: 1.0,
},
)
}),
edges.iter().map(|edge| ((edge.0, edge.1), 1.0)).collect(),
&mut rng,
);
// First run with normal forces to grossly form communities
for _ in 0..1000 {
layout.iteration();
}
// Now collapse into a compact disk using strong gravity
layout.set_settings(forceatlas2::Settings {
theta: 0.1,
ka: 1.0,
kg: 1.0,
kr: 1.0,
lin_log: false,
prevent_overlapping: None,
speed: 0.01,
strong_gravity: true,
});
for _ in 0..1000 {
layout.iteration();
}
let mut x_min = 0.0;
let mut x_max = 0.0;
let mut y_min = 0.0;
let mut y_max = 0.0;
for node in layout.nodes.iter() {
if node.pos.x() < x_min {
x_min = node.pos.x();
}
if node.pos.x() > x_max {
x_max = node.pos.x();
}
if node.pos.y() < y_min {
y_min = node.pos.y();
}
if node.pos.y() > y_max {
y_max = node.pos.y();
}
}
let x_range = x_max - x_min;
let y_range = y_max - y_min;
let x_factor = (width as f32 - node_radius * 2.0) / x_range;
let y_factor = (height as f32 - node_radius * 2.0) / y_range;
let node_size = node_radius / x_factor.min(y_factor);
for node in layout.nodes.iter_mut() {
node.size = node_size;
}
// Finally remove overlapping
layout.set_settings(forceatlas2::Settings {
theta: 0.05,
ka: 1.0,
kg: 1.0,
kr: 1.0,
lin_log: false,
prevent_overlapping: Some(100.0),
speed: 0.01,
strong_gravity: true,
});
for _ in 0..100 {
layout.iteration();
}
let mut x_min = 0.0;
let mut x_max = 0.0;
let mut y_min = 0.0;
let mut y_max = 0.0;
for node in layout.nodes.iter() {
if node.pos.x() < x_min {
x_min = node.pos.x();
}
if node.pos.x() > x_max {
x_max = node.pos.x();
}
if node.pos.y() < y_min {
y_min = node.pos.y();
}
if node.pos.y() > y_max {
y_max = node.pos.y();
}
}
let x_range = x_max - x_min;
let y_range = y_max - y_min;
let x_factor = (width as f32 - node_radius * 2.0) / x_range;
let y_factor = (height as f32 - node_radius * 2.0) / y_range;
let x_mid = (x_min + x_max) * 0.5;
let y_mid = (y_min + y_max) * 0.5;
let mut outfile = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(path)
.unwrap();
outfile
.write_all(
format!(
r#"<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="{}" height="{}" viewBox="{} {} {} {}">
<title>Network graph</title>
<style type="text/css">
.node-label {{
text-anchor: middle;
dominant-baseline: central;
font-size: 12px;
}}
</style>
"#,
width,
height,
width as f32 * -0.5,
height as f32 * -0.5,
width,
height
)
.as_bytes(),
)
.unwrap();
for ((n1, n2), _weight) in layout.edges {
let p1 = layout.nodes[n1].pos;
let p2 = layout.nodes[n2].pos;
outfile
.write_all(
format!(
r#"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="black"/>"#,
(p1.x() - x_mid) * x_factor,
(p1.y() - y_mid) * y_factor,
(p2.x() - x_mid) * x_factor,
(p2.y() - y_mid) * y_factor
)
.as_bytes(),
)
.unwrap();
}
for (i, node) in layout.nodes.iter().enumerate() {
let x = (node.pos.x() - x_mid) * x_factor;
let y = (node.pos.y() - y_mid) * y_factor;
outfile
.write_all(
format!(
r#"<circle cx="{}" cy="{}" r="{}" stroke="black" fill="white"/>"#,
x, y, node_radius
)
.as_bytes(),
)
.unwrap();
outfile
.write_all(
format!(
r#"<text x="{}" y="{}" class="node-label">{}</text>"#,
x, y, i
)
.as_bytes(),
)
.unwrap();
}
outfile.write_all(br#"</svg>"#).unwrap();
outfile.flush().unwrap();
}