From 173d2f430f0fec783130fd01ad42f92531b1f3db Mon Sep 17 00:00:00 2001 From: "Ian Clarke (aider)" Date: Mon, 25 Nov 2024 13:58:33 -0600 Subject: [PATCH] feat: Add interactive small world networks blog post with visualizations --- .../content/news/small-world-networks.md | 35 ++++ .../partials/small-world-routing-viz.html | 33 ++++ .../partials/small-world-scale-viz.html | 37 +++++ .../layouts/partials/small-world-viz.html | 35 ++++ hugo-site/static/js/small-world-routing.js | 150 +++++++++++++++++ hugo-site/static/js/small-world.js | 155 ++++++++++++++++++ 6 files changed, 445 insertions(+) create mode 100644 hugo-site/content/news/small-world-networks.md create mode 100644 hugo-site/layouts/partials/small-world-routing-viz.html create mode 100644 hugo-site/layouts/partials/small-world-scale-viz.html create mode 100644 hugo-site/layouts/partials/small-world-viz.html create mode 100644 hugo-site/static/js/small-world-routing.js create mode 100644 hugo-site/static/js/small-world.js diff --git a/hugo-site/content/news/small-world-networks.md b/hugo-site/content/news/small-world-networks.md new file mode 100644 index 00000000..40249900 --- /dev/null +++ b/hugo-site/content/news/small-world-networks.md @@ -0,0 +1,35 @@ +--- +title: "Understanding Small World Networks" +date: 2024-11-25 +description: "An interactive exploration of small world networks, their properties, and why they matter" +draft: false +--- + +Small world networks are a fascinating type of network structure that appears naturally in many real-world systems - from social networks to the internet's topology. In this interactive post, we'll explore how these networks work and why they're so efficient at routing information. + +## What Makes a Network "Small World"? + +A small world network has a unique property: most nodes are not directly connected, but can reach each other through a surprisingly small number of steps. This is achieved through a specific pattern of connections: + +1. Many short-range connections between nearby nodes +2. A few long-range connections that act as shortcuts + +The visualization below shows this pattern. The nodes are arranged in a circle, with connections colored based on their length. Notice how the link distribution follows a specific pattern: + +{{< partial "small-world-viz.html" >}} + +## Greedy Routing in Small World Networks + +One of the most interesting properties of small world networks is how efficiently they can route messages using only local information. At each step, a node simply passes the message to its neighbor that's closest to the final destination. This "greedy" strategy works remarkably well: + +{{< partial "small-world-routing-viz.html" >}} + +## How Do Small World Networks Scale? + +As we add more nodes to a small world network, how does the average path length between nodes grow? The visualization below shows this relationship: + +{{< partial "small-world-scale-viz.html" >}} + +The remarkable thing about small world networks is that the average path length grows logarithmically with the network size. This means that even in very large networks, most nodes can reach each other in relatively few steps. + +This property helps explain why the "six degrees of separation" phenomenon works in social networks, and why the internet can route packets efficiently despite its enormous size. diff --git a/hugo-site/layouts/partials/small-world-routing-viz.html b/hugo-site/layouts/partials/small-world-routing-viz.html new file mode 100644 index 00000000..d2f1711f --- /dev/null +++ b/hugo-site/layouts/partials/small-world-routing-viz.html @@ -0,0 +1,33 @@ +
+
+ +
+
+ +
+
+ + + + diff --git a/hugo-site/layouts/partials/small-world-scale-viz.html b/hugo-site/layouts/partials/small-world-scale-viz.html new file mode 100644 index 00000000..0c035174 --- /dev/null +++ b/hugo-site/layouts/partials/small-world-scale-viz.html @@ -0,0 +1,37 @@ +
+
+ +
+

Average Path Length vs Network Size

+ +
+
+
+ +
+
+ + + + diff --git a/hugo-site/layouts/partials/small-world-viz.html b/hugo-site/layouts/partials/small-world-viz.html new file mode 100644 index 00000000..740afac5 --- /dev/null +++ b/hugo-site/layouts/partials/small-world-viz.html @@ -0,0 +1,35 @@ +
+
+ +
+
+
+ +
+
+ + + + + diff --git a/hugo-site/static/js/small-world-routing.js b/hugo-site/static/js/small-world-routing.js new file mode 100644 index 00000000..74c85c81 --- /dev/null +++ b/hugo-site/static/js/small-world-routing.js @@ -0,0 +1,150 @@ +// Network parameters +const width = 800; +const height = 800; +const radius = 300; +const numPeers = 30; +const connectionProbability = (distance) => 1 / (distance + 1); + +// Initialize canvas +const canvas = document.getElementById('routingCanvas'); +const ctx = canvas.getContext('2d'); + +let peers = []; +let links = []; +let currentPath = []; +let sourceNode = null; +let targetNode = null; +let animationFrame = null; + +function initializeNetwork() { + // Calculate positions for peers on a 1D ring + peers = d3.range(numPeers).map(i => { + const angle = (i / numPeers) * 2 * Math.PI; + return { + x: width / 2 + radius * Math.cos(angle), + y: height / 2 + radius * Math.sin(angle), + index: i + }; + }); + + // Generate links between peers + links = []; + for (let i = 0; i < numPeers; i++) { + const nextIndex = (i + 1) % numPeers; + links.push({ source: peers[i], target: peers[nextIndex] }); + + for (let j = i + 2; j < numPeers; j++) { + const distance = Math.min(Math.abs(i - j), numPeers - Math.abs(i - j)); + const prob = connectionProbability(distance); + if (Math.random() < prob) { + links.push({ source: peers[i], target: peers[j] }); + } + } + } +} + +function drawNetwork() { + ctx.clearRect(0, 0, width, height); + + // Draw links + ctx.strokeStyle = 'rgba(100, 149, 237, 0.3)'; + ctx.lineWidth = 1; + links.forEach(link => { + ctx.beginPath(); + ctx.moveTo(link.source.x, link.source.y); + ctx.lineTo(link.target.x, link.target.y); + ctx.stroke(); + }); + + // Draw routing path + if (currentPath.length > 1) { + ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(currentPath[0].x, currentPath[0].y); + for (let i = 1; i < currentPath.length; i++) { + ctx.lineTo(currentPath[i].x, currentPath[i].y); + } + ctx.stroke(); + } + + // Draw nodes + peers.forEach(peer => { + if (peer === sourceNode) { + ctx.fillStyle = 'green'; + } else if (peer === targetNode) { + ctx.fillStyle = 'red'; + } else { + ctx.fillStyle = 'tomato'; + } + ctx.beginPath(); + ctx.arc(peer.x, peer.y, 5, 0, 2 * Math.PI); + ctx.fill(); + }); +} + +function findNextHop(current, target) { + let bestNeighbor = null; + let bestDistance = Infinity; + + links.forEach(link => { + let neighbor = null; + if (link.source === current) neighbor = link.target; + if (link.target === current) neighbor = link.source; + + if (neighbor && !currentPath.includes(neighbor)) { + const distance = Math.hypot(neighbor.x - target.x, neighbor.y - target.y); + if (distance < bestDistance) { + bestDistance = distance; + bestNeighbor = neighbor; + } + } + }); + + return bestNeighbor; +} + +function animateRouting() { + if (!currentPath.length || currentPath[currentPath.length - 1] === targetNode) { + cancelAnimationFrame(animationFrame); + return; + } + + const current = currentPath[currentPath.length - 1]; + const next = findNextHop(current, targetNode); + + if (next) { + currentPath.push(next); + drawNetwork(); + } else { + cancelAnimationFrame(animationFrame); + return; + } + + animationFrame = requestAnimationFrame(animateRouting); +} + +function startNewRoute() { + // Cancel any ongoing animation + cancelAnimationFrame(animationFrame); + + // Select random source and target + sourceNode = peers[Math.floor(Math.random() * peers.length)]; + do { + targetNode = peers[Math.floor(Math.random() * peers.length)]; + } while (sourceNode === targetNode); + + // Initialize path with source + currentPath = [sourceNode]; + + // Start animation + animationFrame = requestAnimationFrame(animateRouting); +} + +// Event listeners +document.getElementById('newRouteBtn').addEventListener('click', startNewRoute); + +// Initial setup +initializeNetwork(); +drawNetwork(); +startNewRoute(); diff --git a/hugo-site/static/js/small-world.js b/hugo-site/static/js/small-world.js new file mode 100644 index 00000000..09c1de58 --- /dev/null +++ b/hugo-site/static/js/small-world.js @@ -0,0 +1,155 @@ +// Network parameters +const width = 800; +const height = 800; +const radius = 300; +const numPeers = 30; +const connectionProbability = (distance) => 1 / (distance + 1); + +// Initialize canvas and D3 +const canvas = document.getElementById('networkCanvas'); +const ctx = canvas.getContext('2d'); +const histogram = d3.select('#histogram'); + +let peers = []; +let links = []; + +function initializeNetwork() { + // Calculate positions for peers on a 1D ring + peers = d3.range(numPeers).map(i => { + const angle = (i / numPeers) * 2 * Math.PI; + return { + x: width / 2 + radius * Math.cos(angle), + y: height / 2 + radius * Math.sin(angle), + index: i + }; + }); + + // Generate links between peers + links = []; + for (let i = 0; i < numPeers; i++) { + // Connect adjacent peers + const nextIndex = (i + 1) % numPeers; + links.push({ + source: peers[i], + target: peers[nextIndex], + distance: 1 + }); + + // Additional probabilistic links + for (let j = i + 2; j < numPeers; j++) { + const distance = Math.min(Math.abs(i - j), numPeers - Math.abs(i - j)); + const prob = connectionProbability(distance); + if (Math.random() < prob) { + links.push({ + source: peers[i], + target: peers[j], + distance: distance + }); + } + } + } +} + +function drawNetwork() { + ctx.clearRect(0, 0, width, height); + + // Draw links with color based on distance + links.forEach(link => { + const color = d3.interpolateBlues(1 - link.distance / (numPeers / 2)); + ctx.strokeStyle = color; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(link.source.x, link.source.y); + ctx.lineTo(link.target.x, link.target.y); + ctx.stroke(); + }); + + // Draw nodes + peers.forEach(peer => { + ctx.fillStyle = 'tomato'; + ctx.beginPath(); + ctx.arc(peer.x, peer.y, 5, 0, 2 * Math.PI); + ctx.fill(); + }); +} + +function updateHistogram() { + // Group links by distance + const distanceCounts = d3.rollup( + links, + v => v.length, + d => d.distance + ); + + // Convert to array for D3 + const data = Array.from(distanceCounts, ([distance, count]) => ({ + distance, + count + })).sort((a, b) => a.distance - b.distance); + + // Clear previous histogram + histogram.selectAll('*').remove(); + + // Create SVG + const svg = histogram.append('svg') + .attr('width', width) + .attr('height', 200); + + // Create scales + const x = d3.scaleLinear() + .domain([0, d3.max(data, d => d.distance)]) + .range([40, width - 40]); + + const y = d3.scaleLinear() + .domain([0, d3.max(data, d => d.count)]) + .range([160, 20]); + + // Draw bars + svg.selectAll('rect') + .data(data) + .enter() + .append('rect') + .attr('x', d => x(d.distance)) + .attr('y', d => y(d.count)) + .attr('width', width / (numPeers / 2) - 2) + .attr('height', d => 160 - y(d.count)) + .attr('fill', d => d3.interpolateBlues(1 - d.distance / (numPeers / 2))); + + // Add axes + const xAxis = d3.axisBottom(x); + const yAxis = d3.axisLeft(y); + + svg.append('g') + .attr('transform', 'translate(0, 160)') + .call(xAxis); + + svg.append('g') + .attr('transform', 'translate(40, 0)') + .call(yAxis); + + // Add labels + svg.append('text') + .attr('x', width / 2) + .attr('y', 190) + .attr('text-anchor', 'middle') + .text('Link Distance'); + + svg.append('text') + .attr('transform', 'rotate(-90)') + .attr('x', -80) + .attr('y', 15) + .attr('text-anchor', 'middle') + .text('Number of Links'); +} + +function initialize() { + initializeNetwork(); + drawNetwork(); + updateHistogram(); +} + +// Event listeners +document.getElementById('resetBtn').addEventListener('click', initialize); + +// Initial setup +initialize();