Skip to content

Force Functions (fa2util)

Internal force computation module. These functions exist in three backends:

  1. Cython (.pyx): Compiled C extension for maximum performance
  2. Pure Python (.py): Fallback when Cython is not compiled
  3. Vectorized (fa2util_vectorized.py): NumPy broadcasting backend

Note

Some low-level force functions (linRepulsion, linGravity, strongGravity, etc.) are internal (cdef in Cython) and not part of the public API. They are called by the batch functions below.

Data Classes

Node

Node(dim=2)

Graph node with N-dimensional position and force vectors.

Edge

Edge()

Undirected graph edge with source/target node indices and weight.

Batch Operations

These are the main entry points used by the ForceAtlas2 class:

apply_repulsion

apply_repulsion(nodes, coefficient, adjustSizes=False)

Apply repulsion forces between all pairs of nodes.

Parameters:

Name Type Description Default
nodes list of Node

All graph nodes.

required
coefficient float

Repulsion coefficient (scalingRatio).

required
adjustSizes bool

Use anti-collision repulsion.

False
Source code in fa2/fa2util.py
def apply_repulsion(nodes, coefficient, adjustSizes=False):
    """Apply repulsion forces between all pairs of nodes.

    Parameters
    ----------
    nodes : list of Node
        All graph nodes.
    coefficient : float
        Repulsion coefficient (``scalingRatio``).
    adjustSizes : bool
        Use anti-collision repulsion.
    """
    repulse_fn = linRepulsion_antiCollision if adjustSizes else linRepulsion
    for i, n1 in enumerate(nodes):
        for n2 in nodes[:i]:
            repulse_fn(n1, n2, coefficient)

apply_gravity

apply_gravity(nodes, gravity, scalingRatio, useStrongGravity=False)

Apply gravity forces to all nodes.

Parameters:

Name Type Description Default
nodes list of Node
required
gravity float

Gravity strength.

required
scalingRatio float

Used as coefficient for strong gravity.

required
useStrongGravity bool

Use distance-independent strong gravity.

False
Source code in fa2/fa2util.py
def apply_gravity(nodes, gravity, scalingRatio, useStrongGravity=False):
    """Apply gravity forces to all nodes.

    Parameters
    ----------
    nodes : list of Node
    gravity : float
        Gravity strength.
    scalingRatio : float
        Used as coefficient for strong gravity.
    useStrongGravity : bool
        Use distance-independent strong gravity.
    """
    if not useStrongGravity:
        for n in nodes:
            linGravity(n, gravity)
    else:
        for n in nodes:
            strongGravity(n, gravity, scalingRatio)

apply_attraction

apply_attraction(nodes, edges, distributedAttraction, coefficient, edgeWeightInfluence, linLogMode=False, adjustSizes=False)

Apply attraction forces along all edges.

Selects the appropriate attraction function based on linLogMode and adjustSizes. Optimizes for edgeWeightInfluence of 0 or 1 to avoid slow pow() calls.

Parameters:

Name Type Description Default
nodes list of Node
required
edges list of Edge
required
distributedAttraction bool

Divide by source mass (dissuade hubs).

required
coefficient float

Attraction coefficient.

required
edgeWeightInfluence float

Exponent applied to edge weights.

required
linLogMode bool

Use logarithmic attraction.

False
adjustSizes bool

Use anti-collision attraction.

False
Source code in fa2/fa2util.py
def apply_attraction(nodes, edges, distributedAttraction, coefficient, edgeWeightInfluence, linLogMode=False,
                     adjustSizes=False):
    """Apply attraction forces along all edges.

    Selects the appropriate attraction function based on ``linLogMode``
    and ``adjustSizes``. Optimizes for ``edgeWeightInfluence`` of 0 or 1
    to avoid slow ``pow()`` calls.

    Parameters
    ----------
    nodes : list of Node
    edges : list of Edge
    distributedAttraction : bool
        Divide by source mass (dissuade hubs).
    coefficient : float
        Attraction coefficient.
    edgeWeightInfluence : float
        Exponent applied to edge weights.
    linLogMode : bool
        Use logarithmic attraction.
    adjustSizes : bool
        Use anti-collision attraction.
    """
    if adjustSizes:
        attract_fn = logAttraction_antiCollision if linLogMode else linAttraction_antiCollision
    else:
        attract_fn = logAttraction if linLogMode else linAttraction
    if edgeWeightInfluence == 0:
        for edge in edges:
            attract_fn(nodes[edge.node1], nodes[edge.node2], 1, distributedAttraction, coefficient)
    elif edgeWeightInfluence == 1:
        for edge in edges:
            attract_fn(nodes[edge.node1], nodes[edge.node2], edge.weight, distributedAttraction, coefficient)
    else:
        for edge in edges:
            attract_fn(nodes[edge.node1], nodes[edge.node2], pow(edge.weight, edgeWeightInfluence),
                       distributedAttraction, coefficient)

Attraction Functions

linAttraction

linAttraction(n1, n2, e, distributedAttraction, coefficient=0)

Apply linear attraction between connected nodes.

Force: F = -coefficient * edgeWeight * distance.

Parameters:

Name Type Description Default
n1 Node

Source and target nodes.

required
n2 Node

Source and target nodes.

required
e float

Edge weight (after edgeWeightInfluence exponent).

required
distributedAttraction bool

If True, divide by source node mass (dissuade hubs).

required
coefficient float

Attraction coefficient.

0
Source code in fa2/fa2util.py
def linAttraction(n1, n2, e, distributedAttraction, coefficient=0):
    """Apply linear attraction between connected nodes.

    Force: ``F = -coefficient * edgeWeight * distance``.

    Parameters
    ----------
    n1, n2 : Node
        Source and target nodes.
    e : float
        Edge weight (after ``edgeWeightInfluence`` exponent).
    distributedAttraction : bool
        If True, divide by source node mass (dissuade hubs).
    coefficient : float
        Attraction coefficient.
    """
    dim = n1.dim
    dist = [n1.pos[d] - n2.pos[d] for d in range(dim)]
    if not distributedAttraction:
        factor = -coefficient * e
    else:
        factor = -coefficient * e / n1.mass
    for d in range(dim):
        n1.force[d] += dist[d] * factor
        n2.force[d] -= dist[d] * factor

logAttraction

logAttraction(n1, n2, e, distributedAttraction, coefficient=0)

Apply logarithmic attraction for LinLog mode.

Force: F = -coefficient * edgeWeight * log(1 + distance). Reference: Jacomy et al. 2014 Formula 3; Gephi ForceFactory.java.

Parameters:

Name Type Description Default
n1 Node

Source and target nodes.

required
n2 Node

Source and target nodes.

required
e float

Edge weight.

required
distributedAttraction bool

If True, divide by source node mass.

required
coefficient float

Attraction coefficient.

0
Source code in fa2/fa2util.py
def logAttraction(n1, n2, e, distributedAttraction, coefficient=0):
    """Apply logarithmic attraction for LinLog mode.

    Force: ``F = -coefficient * edgeWeight * log(1 + distance)``.
    Reference: Jacomy et al. 2014 Formula 3; Gephi ``ForceFactory.java``.

    Parameters
    ----------
    n1, n2 : Node
        Source and target nodes.
    e : float
        Edge weight.
    distributedAttraction : bool
        If True, divide by source node mass.
    coefficient : float
        Attraction coefficient.
    """
    dim = n1.dim
    dist = [n1.pos[d] - n2.pos[d] for d in range(dim)]
    distance = sqrt(sum(v * v for v in dist))

    if distance > 0:
        log_factor = log(1 + distance) / distance
        if not distributedAttraction:
            factor = -coefficient * e * log_factor
        else:
            factor = -coefficient * e * log_factor / n1.mass
        for d in range(dim):
            n1.force[d] += dist[d] * factor
            n2.force[d] -= dist[d] * factor

linAttraction_antiCollision

linAttraction_antiCollision(n1, n2, e, distributedAttraction, coefficient=0)

Linear attraction with anti-collision: zero force when overlapping.

Reference: Gephi ForceFactory.java linAttraction_antiCollision.

Parameters:

Name Type Description Default
n1 Node

Source and target nodes.

required
n2 Node

Source and target nodes.

required
e float

Edge weight.

required
distributedAttraction bool

If True, divide by source node mass.

required
coefficient float

Attraction coefficient.

0
Source code in fa2/fa2util.py
def linAttraction_antiCollision(n1, n2, e, distributedAttraction, coefficient=0):
    """Linear attraction with anti-collision: zero force when overlapping.

    Reference: Gephi ``ForceFactory.java`` ``linAttraction_antiCollision``.

    Parameters
    ----------
    n1, n2 : Node
        Source and target nodes.
    e : float
        Edge weight.
    distributedAttraction : bool
        If True, divide by source node mass.
    coefficient : float
        Attraction coefficient.
    """
    dim = n1.dim
    dist = [n1.pos[d] - n2.pos[d] for d in range(dim)]
    euclidean = sqrt(sum(v * v for v in dist))
    distance = euclidean - n1.size - n2.size

    if distance > 0:
        if not distributedAttraction:
            factor = -coefficient * e
        else:
            factor = -coefficient * e / n1.mass
        for d in range(dim):
            n1.force[d] += dist[d] * factor
            n2.force[d] -= dist[d] * factor

logAttraction_antiCollision

logAttraction_antiCollision(n1, n2, e, distributedAttraction, coefficient=0)

Logarithmic attraction with anti-collision: zero force when overlapping.

Parameters:

Name Type Description Default
n1 Node

Source and target nodes.

required
n2 Node

Source and target nodes.

required
e float

Edge weight.

required
distributedAttraction bool

If True, divide by source node mass.

required
coefficient float

Attraction coefficient.

0
Source code in fa2/fa2util.py
def logAttraction_antiCollision(n1, n2, e, distributedAttraction, coefficient=0):
    """Logarithmic attraction with anti-collision: zero force when overlapping.

    Parameters
    ----------
    n1, n2 : Node
        Source and target nodes.
    e : float
        Edge weight.
    distributedAttraction : bool
        If True, divide by source node mass.
    coefficient : float
        Attraction coefficient.
    """
    dim = n1.dim
    dist = [n1.pos[d] - n2.pos[d] for d in range(dim)]
    euclidean = sqrt(sum(v * v for v in dist))
    distance = euclidean - n1.size - n2.size

    if distance > 0:
        log_factor = log(1 + distance) / distance
        if not distributedAttraction:
            factor = -coefficient * e * log_factor
        else:
            factor = -coefficient * e * log_factor / n1.mass
        for d in range(dim):
            n1.force[d] += dist[d] * factor
            n2.force[d] -= dist[d] * factor

Speed Adjustment

adjustSpeedAndApplyForces

adjustSpeedAndApplyForces(nodes, speed, speedEfficiency, jitterTolerance, adjustSizes=False)

Adjust simulation speed and apply accumulated forces to node positions.

Uses swing/traction measurement to adaptively control speed. High swing (oscillation) reduces speed; high traction (consistent movement) increases it.

Parameters:

Name Type Description Default
nodes list of Node

All graph nodes with accumulated forces.

required
speed float

Current simulation speed.

required
speedEfficiency float

Current speed efficiency factor.

required
jitterTolerance float

How much swinging is tolerated.

required
adjustSizes bool

Cap per-node speed based on node size.

False

Returns:

Type Description
dict

{'speed': float, 'speedEfficiency': float}

Source code in fa2/fa2util.py
def adjustSpeedAndApplyForces(nodes, speed, speedEfficiency, jitterTolerance, adjustSizes=False):
    """Adjust simulation speed and apply accumulated forces to node positions.

    Uses swing/traction measurement to adaptively control speed.
    High swing (oscillation) reduces speed; high traction (consistent
    movement) increases it.

    Parameters
    ----------
    nodes : list of Node
        All graph nodes with accumulated forces.
    speed : float
        Current simulation speed.
    speedEfficiency : float
        Current speed efficiency factor.
    jitterTolerance : float
        How much swinging is tolerated.
    adjustSizes : bool
        Cap per-node speed based on node size.

    Returns
    -------
    dict
        ``{'speed': float, 'speedEfficiency': float}``
    """
    totalSwinging = 0.0
    totalEffectiveTraction = 0.0
    for n in nodes:
        dim = n.dim
        swinging = sqrt(sum((n.old_force[d] - n.force[d]) ** 2 for d in range(dim)))
        totalSwinging += n.mass * swinging
        totalEffectiveTraction += .5 * n.mass * sqrt(
            sum((n.old_force[d] + n.force[d]) ** 2 for d in range(dim)))

    estimatedOptimalJitterTolerance = .05 * sqrt(len(nodes))
    minJT = sqrt(estimatedOptimalJitterTolerance)
    maxJT = 10
    if len(nodes) > 0 and totalEffectiveTraction > 0:
        jt = jitterTolerance * max(minJT,
                                   min(maxJT, estimatedOptimalJitterTolerance * totalEffectiveTraction / (
                                       len(nodes) * len(nodes))))
    else:
        jt = jitterTolerance * minJT

    minSpeedEfficiency = 0.05

    if totalEffectiveTraction > 0 and totalSwinging / totalEffectiveTraction > 2.0:
        if speedEfficiency > minSpeedEfficiency:
            speedEfficiency *= .5
        jt = max(jt, jitterTolerance)

    if totalSwinging == 0:
        targetSpeed = float('inf')
    else:
        targetSpeed = jt * speedEfficiency * totalEffectiveTraction / totalSwinging

    if totalSwinging > jt * totalEffectiveTraction:
        if speedEfficiency > minSpeedEfficiency:
            speedEfficiency *= .7
    elif speed < 1000:
        speedEfficiency *= 1.3

    maxRise = .5
    speed = speed + min(targetSpeed - speed, maxRise * speed)

    # Apply forces to positions
    for n in nodes:
        dim = n.dim
        swinging = n.mass * sqrt(sum((n.old_force[d] - n.force[d]) ** 2 for d in range(dim)))
        factor = speed / (1.0 + sqrt(speed * swinging))
        # Gephi caps per-node speed when adjustSizes is on to prevent large nodes overshooting
        if adjustSizes and n.size > 0:
            factor = min(factor, 10.0 / n.size)
        for d in range(dim):
            n.pos[d] += n.force[d] * factor

    return {'speed': speed, 'speedEfficiency': speedEfficiency}

Barnes-Hut Spatial Tree

Region

Region(nodes)

Barnes-Hut spatial tree node.

Generalizes the quadtree to 2^dim partitioning for N-dimensional layouts. Each region stores aggregated mass and center of mass for its child nodes, enabling O(n log n) repulsion approximation.

Parameters:

Name Type Description Default
nodes list of Node

Nodes contained in this region.

required