The fastest ForceAtlas2 algorithm for Python (and NetworkX)
Project description
ForceAtlas2 for Python
The fastest Python implementation of the ForceAtlas2 graph layout algorithm, with Cython optimization for 10-100x speedup. Supports NetworkX, igraph, and raw adjacency matrices.
ForceAtlas2 is a force-directed layout algorithm designed for network visualization. It spatializes weighted undirected graphs in 2D, where edge weights define connection strength. It scales well to large graphs (>10,000 nodes) using Barnes-Hut approximation (O(n log n) complexity).
500-node stochastic block model (7 communities) laid out with ForceAtlas2 LinLog mode
Random geometric graph (400 nodes) laid out with ForceAtlas2
Installation
pip install fa2
For maximum performance, install with Cython (recommended):
pip install cython
pip install fa2 --no-binary fa2
To build from source:
git clone https://github.com/bhargavchippada/forceatlas2.git
cd forceatlas2
pip install cython numpy
pip install -e ".[dev]" --no-build-isolation
Dependencies
| Package | Required | Purpose |
|---|---|---|
| numpy | Yes | Adjacency matrix handling |
| scipy | Yes | Sparse matrix support |
| tqdm | Yes | Progress bar |
| cython | No (recommended) | 10-100x speedup |
| networkx | No | NetworkX graph wrapper |
| igraph | No | igraph graph wrapper |
Python: >= 3.9 (tested on 3.9 through 3.14)
Quick Start
With NetworkX
import networkx as nx
import matplotlib.pyplot as plt
from fa2 import ForceAtlas2
G = nx.random_geometric_graph(400, 0.2)
forceatlas2 = ForceAtlas2(
outboundAttractionDistribution=True, # Dissuade hubs
edgeWeightInfluence=1.0,
jitterTolerance=1.0,
barnesHutOptimize=True,
barnesHutTheta=1.2,
scalingRatio=2.0,
strongGravityMode=False,
gravity=1.0,
verbose=True,
)
positions = forceatlas2.forceatlas2_networkx_layout(G, pos=None, iterations=2000)
nx.draw_networkx_nodes(G, positions, node_size=20, node_color="blue", alpha=0.4)
nx.draw_networkx_edges(G, positions, edge_color="green", alpha=0.05)
plt.axis("off")
plt.show()
With Raw Adjacency Matrix
import numpy as np
from fa2 import ForceAtlas2
# Create a symmetric adjacency matrix
G = np.array([
[0, 1, 1, 0],
[1, 0, 1, 1],
[1, 1, 0, 0],
[0, 1, 0, 0],
], dtype=float)
forceatlas2 = ForceAtlas2(verbose=False, seed=42)
positions = forceatlas2.forceatlas2(G, iterations=1000)
# Returns: [(x1, y1), (x2, y2), ...]
With Scipy Sparse Matrix
import scipy.sparse
from fa2 import ForceAtlas2
# For large graphs, sparse matrices are more memory-efficient
G_sparse = scipy.sparse.csr_matrix(adjacency_matrix)
forceatlas2 = ForceAtlas2(verbose=False)
positions = forceatlas2.forceatlas2(G_sparse, iterations=1000)
With igraph
import igraph
from fa2 import ForceAtlas2
G = igraph.Graph.Famous("Petersen")
forceatlas2 = ForceAtlas2(verbose=False)
layout = forceatlas2.forceatlas2_igraph_layout(G, iterations=1000)
igraph.plot(G, layout=layout)
API Reference
ForceAtlas2(**kwargs)
Create a ForceAtlas2 layout engine with the following parameters:
Behavior
| Parameter | Type | Default | Description |
|---|---|---|---|
outboundAttractionDistribution |
bool | False |
Dissuade hubs — distributes attraction along outbound edges so hubs are pushed to borders. Each edge's attraction is divided by the source node's degree+1 |
linLogMode |
bool | False |
Use Noack's LinLog model: F = log(1 + distance) instead of F = distance. Produces tighter community clusters with clearer separation between groups |
edgeWeightInfluence |
float | 1.0 |
How much edge weights matter. 0 = all edges equal, 1 = normal, values between apply weight^influence |
Performance
| Parameter | Type | Default | Description |
|---|---|---|---|
barnesHutOptimize |
bool | True |
Use Barnes-Hut quadtree approximation for repulsion. Reduces O(n^2) to O(n log n). Essential for graphs >1000 nodes |
barnesHutTheta |
float | 1.2 |
Barnes-Hut accuracy/speed tradeoff. Lower = more accurate but slower. Gephi default: 1.2 |
jitterTolerance |
float | 1.0 |
How much oscillation is tolerated during convergence. Higher = faster but less precise. Values >1 discouraged |
Tuning
| Parameter | Type | Default | Description |
|---|---|---|---|
scalingRatio |
float | 2.0 |
Repulsion strength. Higher = more spread out graph. Must be > 0 |
strongGravityMode |
bool | False |
Distance-independent gravity: constant-magnitude pull regardless of distance from center. Standard gravity weakens with distance |
gravity |
float | 1.0 |
Center attraction strength. Prevents disconnected components from drifting. Must be >= 0 |
Other
| Parameter | Type | Default | Description |
|---|---|---|---|
seed |
int/None | None |
Random seed for reproducible layouts. Same seed + same graph = same positions |
verbose |
bool | True |
Show progress bar (tqdm) and timing breakdown to stdout |
Methods
forceatlas2(G, pos=None, iterations=100, callbacks=None)
Compute layout from an adjacency matrix.
- G:
numpy.ndarrayorscipy.sparsematrix (must be symmetric) - pos: Initial positions as
(N, 2)array, orNonefor random initialization - iterations: Number of layout iterations
- callbacks: List of
callback(iteration, nodes)functions called after each iteration - Returns: List of
(x, y)tuples, one per node
forceatlas2_networkx_layout(G, pos=None, iterations=100, weight_attr=None, callbacks=None)
Compute layout for a NetworkX graph. Supports NetworkX 2.7+ and 3.x.
- G:
networkx.Graph(undirected) - pos: Initial positions as
{node: (x, y)}dict - weight_attr: Edge attribute name for weights (e.g.,
"weight","strength") - callbacks: List of
callback(iteration, nodes)functions - Returns: Dict of
{node: (x, y)}
forceatlas2_igraph_layout(G, pos=None, iterations=100, weight_attr=None, callbacks=None)
Compute layout for an igraph graph.
- G:
igraph.Graph(must be undirected) - pos: Initial positions as list or
(N, 2)numpy array - weight_attr: Edge attribute name for weights
- callbacks: List of
callback(iteration, nodes)functions - Returns:
igraph.Layout
Advanced Usage
Reproducible Layouts
Use the seed parameter for deterministic results:
fa = ForceAtlas2(seed=42, verbose=False)
pos1 = fa.forceatlas2_networkx_layout(G, iterations=1000)
fa2 = ForceAtlas2(seed=42, verbose=False)
pos2 = fa2.forceatlas2_networkx_layout(G, iterations=1000)
# pos1 == pos2 guaranteed
LinLog Mode (Community Detection)
LinLog mode replaces the linear attraction force F = distance with a logarithmic one F = log(1 + distance) (Noack's LinLog energy model). This produces layouts where communities form tighter, more clearly separated clusters:
fa = ForceAtlas2(linLogMode=True, verbose=False)
positions = fa.forceatlas2_networkx_layout(G, iterations=2000)
The attraction grows only logarithmically with distance, so distant connected nodes are pulled less strongly relative to repulsion, naturally emphasizing community structure.
Dissuade Hubs
Push high-degree nodes to the periphery by distributing attraction force across outbound edges:
fa = ForceAtlas2(outboundAttractionDistribution=True, verbose=False)
positions = fa.forceatlas2_networkx_layout(G, iterations=2000)
Each edge's attraction is divided by the source node's mass (degree + 1), so hub nodes with many connections experience less total attraction pull. An outboundAttCompensation factor (mean node mass) is applied to maintain overall force balance.
Iteration Callbacks (Animation / History)
Track positions over time for animation or convergence analysis:
history = []
def record_positions(iteration, nodes):
if iteration % 100 == 0:
history.append([(n.x, n.y) for n in nodes])
fa = ForceAtlas2(verbose=False, seed=42)
final_pos = fa.forceatlas2(G, iterations=1000, callbacks=[record_positions])
# history contains snapshots every 100 iterations
Custom Edge Weights
import networkx as nx
G = nx.Graph()
G.add_edge("A", "B", strength=5.0)
G.add_edge("B", "C", strength=1.0)
G.add_edge("A", "C", strength=0.5)
fa = ForceAtlas2(edgeWeightInfluence=1.0, verbose=False)
pos = fa.forceatlas2_networkx_layout(G, weight_attr="strength", iterations=1000)
Tuning Tips
| Goal | Settings |
|---|---|
| Spread out | Increase scalingRatio (e.g., 10.0) |
| Compact | Decrease scalingRatio (e.g., 0.5), increase gravity |
| Community clusters | Enable linLogMode=True |
| Prevent hub dominance | Enable outboundAttractionDistribution=True |
| Faster convergence | Increase jitterTolerance (e.g., 5.0) |
| Higher quality | More iterations, lower jitterTolerance |
| Large graphs (>5000) | Keep barnesHutOptimize=True (default) |
| Strong gravity | Set strongGravityMode=True for constant-magnitude pull (prevents distant nodes from drifting) |
Performance
The Cython-compiled version provides 10-100x speedup over pure Python:
| Graph Size | Edges | Iterations | Pure Python | With Cython | Speedup |
|---|---|---|---|---|---|
| 50 nodes | ~400 | 100 | ~77ms | ~3ms | ~25x |
| 200 nodes | ~1,900 | 50 | ~245ms | ~9ms | ~26x |
| 500 nodes | ~4,900 | 20 | ~327ms | ~17ms | ~19x |
| 10,000 nodes | ~100,000 | 10 | — | ~343ms | — |
| 100,000 nodes | ~500,000 | 5 | — | ~3.0s | — |
Benchmarks on Ubuntu Linux, Python 3.13, Cython 3.2. Barnes-Hut enabled (default). Run pytest tests/test_benchmark.py --benchmark-only -s to reproduce the small-graph benchmarks.
To verify Cython is active:
import fa2.fa2util
print(fa2.fa2util.__file__) # Should end in .so (Linux/Mac) or .pyd (Windows), not .py
Algorithm
Based on the paper:
Jacomy M, Venturini T, Heymann S, Bastian M (2014) ForceAtlas2, a Continuous Graph Layout Algorithm for Handy Network Visualization Designed for the Gephi Software. PLoS ONE 9(6): e98679. https://doi.org/10.1371/journal.pone.0098679
The implementation follows the Gephi Java source and has been verified against both the paper and the reference code.
Force Model
ForceAtlas2 uses a "(1, 1)" energy model — inverse-distance repulsion and linear attraction:
| Force | Formula | Description |
|---|---|---|
| Repulsion | F = k_r * m1 * m2 / d |
All node pairs repel. Mass = degree + 1. Barnes-Hut quadtree approximation reduces O(n^2) to O(n log n) |
| Linear Attraction | F = -c * w * d |
Connected nodes attract proportionally to distance and edge weight |
| Log Attraction | F = -c * w * log(1 + d) |
LinLog mode: sub-linear attraction for community emphasis |
| Gravity | F = m * g / d |
Constant-magnitude pull toward center (standard mode) |
| Strong Gravity | F = c * m * g |
Distance-proportional pull toward center |
Adaptive Speed
Each iteration measures swinging (erratic oscillation) and traction (useful movement) across all nodes. Global speed is set proportional to traction / swinging, with per-node damping for oscillating nodes. This allows fast convergence while preventing instability.
Barnes-Hut Approximation
A quadtree recursively partitions the 2D space. For distant node groups, repulsion is computed against the group's center of mass instead of individual nodes. The barnesHutTheta parameter (default 1.2) controls the distance/size threshold — higher values are faster but less accurate.
Migration from v0.3.x
v0.9.0 is backwards compatible with v0.3.x. Key changes:
| Change | v0.3.x | v0.9.0 |
|---|---|---|
| Python support | 2.7, 3.x | 3.9+ only |
| NetworkX | 2.x only | 2.7+ and 3.x |
| Cython | 0.29.x | 3.x |
linLogMode |
Not implemented | Implemented (correct log(1+d) formula) |
seed parameter |
Not available | New — for reproducibility |
callbacks |
Not available | New — for animation/monitoring |
| igraph support | Fragile | Robust (handles weighted, edgeless, directed-rejection) |
| Error handling | assert statements |
Proper ValueError/TypeError with messages |
| Input validation | Minimal | Symmetry (dense + sparse), pos shape, param ranges, self-loop warning |
| Barnes-Hut | Double-counting leaf repulsion | Correct one-sided repulsion (matches Gephi) |
| Directed graphs | Confusing error | Explicit rejection with helpful message |
adjustSizes |
Silent no-op | Raises NotImplementedError |
multiThreaded |
Silent no-op | Raises NotImplementedError |
Breaking changes
- Python 2 dropped: Python 2.x is no longer supported.
adjustSizes=Truenow raisesNotImplementedErrorinstead of being silently ignored.multiThreaded=Truenow raisesNotImplementedErrorinstead of being silently ignored.- Invalid parameter values (negative
scalingRatio, etc.) now raiseValueError.
Development
# Clone and install
git clone https://github.com/bhargavchippada/forceatlas2.git
cd forceatlas2
pip install cython numpy
pip install -e ".[dev]" --no-build-isolation
# Run tests (112 total: 104 unit/integration + 8 benchmarks)
pytest tests/ -v
# Run tests with coverage (100% on forceatlas2.py)
pytest tests/test_fa2util.py tests/test_forceatlas2.py --cov=fa2 --cov-report=term-missing
# Run benchmarks only
pytest tests/test_benchmark.py --benchmark-only -s
# Lint
ruff check fa2/ tests/
# Regenerate C file after modifying fa2util.pyx
cython fa2/fa2util.pyx -3 -o fa2/fa2util.c
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Ensure all tests pass (100% coverage on
forceatlas2.py) - Submit a pull request
Areas needing help
adjustSizes: Prevent node overlap using anti-collision forces (Gephi's "Prevent Overlap"). Requires new repulsion/attraction variants that subtract node sizes from distances. SeeForceFactory.javafor reference.multiThreaded: Parallel force computation. Gephi parallelizes repulsion + gravity (not attraction) with thread pooling.- N-dimensional layout: Support for 3D and higher-dimensional layouts. The algorithm generalizes naturally — NetworkX's FA2 and the Rust
forceatlas2crate already support this. - NumPy vectorization: Eliminate Python loops in the pure-Python fallback for 5-20x speedup without Cython.
normalizeEdgeWeights: Min-max normalization of edge weights to [0, 1] (Gephi feature).inferSettings(): Auto-tuning of parameters based on graph size and density (from Graphology/sigma.js).
License
Copyright (C) 2017 Bhargav Chippada bhargavchippada19@gmail.com
Licensed under the GNU GPLv3.
Based on the Gephi ForceAtlas2 plugin:
Copyright 2008-2011 Gephi
Authors: Mathieu Jacomy <mathieu.jacomy@gmail.com>
Licensed under GPL v3 / CDDL
And Max Shinn's Python port:
Copyright 2016 Max Shinn <mws41@cam.ac.uk>
Available under the GPLv3
Also thanks to Eugene Bosiakov (https://github.com/bosiakov/fa2l).
2D Grid graph (25x25) laid out with ForceAtlas2
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file fa2-0.9.0.tar.gz.
File metadata
- Download URL: fa2-0.9.0.tar.gz
- Upload date:
- Size: 2.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f692a853edba2c1192b54781871116ae8d072f0009971987da2b81ab3811df5d
|
|
| MD5 |
a1a6a1890dffe4e6807380e323ca4f79
|
|
| BLAKE2b-256 |
ff3f820c58f54d2537b4b78ba9cf813d2620aab3ba8cd09fbd4590d07e690e8a
|