diff --git a/Cargo.lock b/Cargo.lock index b18a59b..d938863 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. 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]] name = "argp" version = "0.4.0" @@ -33,19 +24,10 @@ dependencies = [ ] [[package]] -name = "assert_approx_eq" -version = "1.1.0" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd" - -[[package]] -name = "assert_unordered" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74323b7881323eb351134e08ee5331594826789557afef8e309baf481b2264" -dependencies = [ - "ansi_term", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" @@ -53,6 +35,12 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "cfg-if" version = "1.0.3" @@ -84,18 +72,25 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "getopts" version = "0.2.24" @@ -116,40 +111,21 @@ dependencies = [ "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]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "memchr" version = "2.7.6" @@ -157,10 +133,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] -name = "nohash" -version = "0.2.0" +name = "num-traits" +version = "0.2.19" 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]] name = "ppv-lite86" @@ -192,15 +194,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - [[package]] name = "quote" version = "1.0.41" @@ -260,49 +253,35 @@ dependencies = [ "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]] name = "reticulum-synthesis" version = "0.1.0" dependencies = [ "argp", - "graphrs", + "forceatlas2", + "rand", ] [[package]] -name = "serde" -version = "1.0.228" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde_core" -version = "1.0.228" +name = "smallvec" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -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" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "syn" @@ -340,26 +319,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -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" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "zerocopy" diff --git a/Cargo.toml b/Cargo.toml index f12af5d..76ed36e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,16 @@ authors = ["tuxmain "] repository = "https://git.txmn.tk/tuxmain/reticulum-synthesis" license = "AGPL-3.0-only" +[features] +default = ["render"] + +# Render graph to SVG +render = ["dep:forceatlas2"] + [dependencies] argp = "0.4.0" -graphrs = "0.11.13" +forceatlas2 = { version = "0.8.0", optional = true } +rand = "0.8.5" [profile.release] lto = true diff --git a/README.md b/README.md index cc02f64..40f5f5a 100644 --- a/README.md +++ b/README.md @@ -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. +![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. 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`) +Tune the graph density with the `-p ` option that accepts an edge probability number between 0 and 1. + +When generating a graph, option `-R ` 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) ``` diff --git a/graph.svg b/graph.svg new file mode 100644 index 0000000..8e0b348 --- /dev/null +++ b/graph.svg @@ -0,0 +1,11 @@ + + + Network graph + +012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 \ No newline at end of file diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..995d19d --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,35 @@ +use rand::Rng; + +fn get_neighbors(node: usize, edges: &[(usize, usize)]) -> impl Iterator { + 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::() < proba { + new_friends.push(neighbor); + } + } + for new_friend in new_friends { + edges.push((new_friend, friend)); + } + } + edges +} diff --git a/src/main.rs b/src/main.rs index 82de5fb..8a097f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +mod generator; +mod render; + use std::{ collections::{HashMap, hash_map::Entry}, io::Write, @@ -19,6 +22,10 @@ struct Cli { /// Probability of edge #[argp(option, short = 'p', default = "0.5")] proba: f32, + /// Output SVG image of the graph (only with --gen) + #[cfg(feature = "render")] + #[argp(option, short = 'R')] + render: Option, /// Start the Reticulum nodes #[argp(switch, short = 'r')] run: bool, @@ -28,17 +35,22 @@ fn main() { let cli: Cli = argp::parse_args_or_exit(argp::DEFAULT); if cli.generate { - let graph = graphrs::generators::random::fast_gnp_random_graph( - cli.nodes as _, - cli.proba as _, - false, - None, - ) - .unwrap(); + let mut rng = rand::thread_rng(); + + assert!( + cli.proba >= 0.0 && cli.proba <= 1.0, + "Probability should be between 0 and 1." + ); + let edges = generator::generate(cli.nodes, cli.proba, &mut rng); let mut ports = HashMap::new(); 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); std::fs::DirBuilder::new() @@ -76,11 +88,17 @@ loglevel = 4 .as_bytes(), ) .unwrap(); - for edge in graph.get_edges_for_node(node as _).unwrap() { - let key = if edge.u < edge.v { - (edge.u, edge.v) + for edge in edges.iter().filter_map(|(n1, n2)| { + if *n1 == node || *n2 == node { + Some((*n1, *n2)) } 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); match ports.entry(key) { @@ -103,7 +121,7 @@ listen_port = {} forward_ip = 127.0.0.1 forward_port = {} ", - edge.u, edge.v, edge_ports.0, edge_ports.1 + edge.0, edge.1, edge_ports.0, edge_ports.1 ) .as_bytes(), ) diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..7d6e15a --- /dev/null +++ b/src/render.rs @@ -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::::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#" + + Network graph + +"#, + 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#""#, + (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#""#, + x, y, node_radius + ) + .as_bytes(), + ) + .unwrap(); + outfile + .write_all( + format!( + r#"{}"#, + x, y, i + ) + .as_bytes(), + ) + .unwrap(); + } + outfile.write_all(br#""#).unwrap(); + outfile.flush().unwrap(); +}