Skip to content

fdm

fdm(coords, load, support, edge_index, force_density, batch=None, use_batching=True, directed=False, solve_only_z=False, C=None)

Performs the Force Density Method (FDM) to find the equilibrium state of a structure given its coordinates, loads, supports, edge connections, and force densities.

Parameters:

Name Type Description Default
coords Tensor

A tensor of shape (num_nodes, 3) containing the initial 3D coordinates of each node. Only coordinates of supported nodes have an effect on the output.

required
load Tensor

A tensor of shape (num_nodes, 3) containing the external load vector for each node.

required
support Tensor

A boolean tensor of shape (num_nodes, 1) indicating which nodes are fixed (True) or free (False).

required
edge_index Tensor

A tensor of shape (2, num_edges) containing the indices of the nodes that form each edge.

required
force_density Tensor

A tensor of shape (num_edges, 1) containing the force density for each edge.

required
batch Tensor

A tensor of shape (num_nodes, 1) indicating the batch assignment for each node. If not supplied a single linear system containing all nodes and edges of the entire batch is solved. Default is None.

None
use_batching bool

If True, batching is used for solving the linear system. Default is True.

True
directed bool

If True, edges are assumed to be directed. If False, the force densities are halved to account for undirected edges. Default is False.

False
solve_only_z bool

If True, only the z-coordinate of the free nodes is solved. Default is False.

False
C Tensor

A precomputed branch-node matrix. If None, it is created using the edge_index. Default is None.

None

Returns:

Type Description

A tuple containing: - coords (torch.Tensor): A tensor of shape (num_nodes, 3) containing form-found coordinates of each node. - force (torch.Tensor): A tensor of shape (num_edges, 1) containing the axial force of each edge.

Source code in src/torch_structure/formfinding/fdm.py
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def fdm(
    coords,
    load,
    support,
    edge_index,
    force_density,
    batch=None,
    use_batching=True,
    directed=False,
    solve_only_z=False,
    C=None,
):
    """
    Performs the Force Density Method (FDM) to find the equilibrium state of a structure given its coordinates, loads,
    supports, edge connections, and force densities.

    Args:
        coords (torch.Tensor): A tensor of shape (num_nodes, 3) containing the initial 3D coordinates of each node. Only
            coordinates of supported nodes have an effect on the output.
        load (torch.Tensor): A tensor of shape (num_nodes, 3) containing the external load vector for each node.
        support (torch.Tensor): A boolean tensor of shape (num_nodes, 1) indicating which nodes are fixed (True) or free (False).
        edge_index (torch.Tensor): A tensor of shape (2, num_edges) containing the indices of the nodes that form each edge.
        force_density (torch.Tensor): A tensor of shape (num_edges, 1) containing the force density for each edge.
        batch (torch.Tensor, optional): A tensor of shape (num_nodes, 1) indicating the batch assignment for each node. If not supplied
            a single linear system containing all nodes and edges of the entire batch is solved. Default is None.
        use_batching (bool, optional): If True, batching is used for solving the linear system. Default is True.
        directed (bool, optional): If True, edges are assumed to be directed. If False, the force densities are halved to account for
            undirected edges. Default is False.
        solve_only_z (bool, optional): If True, only the z-coordinate of the free nodes is solved. Default is False.
        C (torch.Tensor, optional): A precomputed branch-node matrix. If None, it is created using the edge_index. Default is None.

    Returns:
        A tuple containing:
            - coords (torch.Tensor): A tensor of shape (num_nodes, 3) containing form-found coordinates of each node.
            - force (torch.Tensor): A tensor of shape (num_edges, 1) containing the axial force of each edge.
    """
    support = support.view(-1)
    force_density = force_density.view(-1)

    device = coords.device
    coords = torch.clone(coords)  # check if this is necessary

    # halve force densities to account for undirected graph
    if not directed:
        force_density = 0.5 * force_density

    if C is None:
        C = create_branch_node_matrix(edge_index)
    C_free = C[:, ~support]
    C_fixed = C[:, support]
    C_free_transposed = torch.transpose(C_free, 0, 1)

    Q = torch.diag(force_density)
    A = torch.mm(torch.mm(C_free_transposed, Q), C_free)  # >1/3 of computation time

    D_f = torch.mm(torch.mm(C_free_transposed, Q), C_fixed)
    if solve_only_z:
        b_1 = load[~support, 2]
        b_2 = torch.mv(D_f, coords[support, 2])
    else:
        b_1 = load[~support]
        b_2 = torch.mm(D_f, coords[support])
    b = b_1 - b_2

    if batch is None:
        form_found_coords = torch.linalg.solve(A, b)
    else:
        batch_free = batch[~support]
        n_graphs = batch.max().item() + 1
        node_counts = torch.bincount(batch)
        max_size = node_counts.max().item()

        identity = torch.eye(max_size, device=device)
        A_batched = identity.unsqueeze(0).repeat(n_graphs, 1, 1)
        b_batched = torch.zeros(n_graphs, max_size, 3, device=device)
        pad_mask = torch.zeros(n_graphs * max_size, dtype=torch.bool, device=device)
        ff_coords = []
        for i in range(n_graphs):
            mask = batch_free == i

            indices = torch.nonzero(mask).squeeze()
            submatrix = A[indices][:, indices]
            A_batched[i, : submatrix.size(0), : submatrix.size(1)] = submatrix

            subvector = b[mask]
            graph_size = subvector.size(0)
            b_batched[i, :graph_size] = subvector

            pad_mask[i * max_size : i * max_size + graph_size] = 1

            if not use_batching:
                ff_coords.append(torch.linalg.solve(submatrix, subvector))
                # ff_coords.append(torch.linalg.solve(A_batched[i], b_batched[i]))

        if use_batching:
            form_found_coords = torch.linalg.solve(A_batched, b_batched)
            form_found_coords = form_found_coords.reshape(n_graphs * max_size, 3)
            form_found_coords = form_found_coords[pad_mask]
        else:
            form_found_coords = torch.cat(ff_coords, dim=0)
            # form_found_coords = form_found_coords.reshape(n_graphs*max_size, 3)[pad_mask]

    if solve_only_z:
        coords[~support, 2] = form_found_coords
    else:
        coords[~support, :] = form_found_coords

    lengths = torch.norm(torch.mm(C, coords), dim=1)
    force = force_density * lengths

    # double force to account for undirected graph
    if not directed:
        force = 2 * force

    return coords, force.unsqueeze(1)