Skip to content

Commit 7bb891e

Browse files
committed
Add c++ solver
Force density solver implemented in C++ through pybind11. The solver gets a speed bump, especially for medium-sized networks (5-20x). Exposes fd_solve() in the fdm.pyd library. Current built for win64. A makefile for other platforms still needs to be included.
1 parent 1bd932c commit 7bb891e

16 files changed

+459
-1
lines changed

scripts/test_cpp.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import compas
2+
from compas.datastructures import mesh_subdivide
3+
from compas_fd.datastructures import CableMesh
4+
from compas_fd.fd import fd_cpp, fd_numpy
5+
from compas_view2.app import App
6+
from compas_view2.objects import Object, MeshObject
7+
from timeit import timeit
8+
9+
Object.register(CableMesh, MeshObject)
10+
11+
12+
# =================================================
13+
# input mesh
14+
# =================================================
15+
16+
mesh = CableMesh.from_obj(compas.get('faces.obj'))
17+
18+
mesh.vertices_attribute('is_anchor', True, mesh.vertices_where({'vertex_degree': 2}))
19+
dva = {'rx': .0, 'ry': .0, 'rz': .0,
20+
'px': .0, 'py': .0, 'pz': -.0,
21+
'is_anchor': False}
22+
23+
mesh = mesh_subdivide(mesh, scheme='quad', k=1)
24+
mesh.update_default_vertex_attributes(dva)
25+
26+
27+
# =================================================
28+
# pre-process mesh data
29+
# =================================================
30+
31+
vertices = [mesh.vertex_coordinates(v) for v in mesh.vertices()]
32+
fixed = list(mesh.vertices_where({'is_anchor': True}))
33+
edges = list(mesh.edges())
34+
force_densities = mesh.edges_attribute('q')
35+
loads = mesh.vertices_attributes(['px', 'py', 'pz'])
36+
37+
38+
# =================================================
39+
# solvers
40+
# =================================================
41+
42+
result_eigen = fd_cpp(vertices=vertices, fixed=fixed, edges=edges,
43+
force_densities=force_densities, loads=loads)
44+
45+
result_np = fd_numpy(vertices=vertices, fixed=fixed, edges=edges,
46+
forcedensities=force_densities, loads=loads)
47+
48+
forces_comparison = ((abs(l1 - l2) < 1E-14) for l1, l2
49+
in zip(result_eigen.forces, result_np.forces))
50+
print("Check if forces are equal between solvers: ", all(forces_comparison))
51+
52+
53+
for vertex, coo in zip(mesh.vertices(), result_eigen.vertices):
54+
mesh.vertex_attributes(vertex, 'xyz', coo)
55+
56+
57+
# =================================================
58+
# benchmarks
59+
# =================================================
60+
61+
def fn_cpp():
62+
fd_cpp(vertices=vertices, fixed=fixed, edges=edges,
63+
force_densities=force_densities, loads=loads)
64+
65+
66+
def fn_numpy():
67+
fd_numpy(vertices=vertices, fixed=fixed, edges=edges,
68+
forcedensities=force_densities, loads=loads)
69+
70+
71+
nr = 30
72+
print(f"Solver time in cpp: {round(1000 * timeit(fn_cpp, number=nr) / nr, 2)} ms")
73+
print(f"Solver time in numpy: {round(1000 * timeit(fn_numpy, number=nr) / nr, 2)} ms")
74+
75+
76+
# =================================================
77+
# viz
78+
# =================================================
79+
80+
viewer = App()
81+
viewer.add(mesh)
82+
viewer.run()

src/compas_fd/fd/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
1616
fd_numpy
1717
mesh_fd_numpy
18+
fd_cpp
1819
1920
"""
2021
from __future__ import print_function
@@ -26,11 +27,13 @@
2627
if not compas.IPY:
2728
from .fd_numpy import fd_numpy
2829
from .mesh_fd_numpy import mesh_fd_numpy
30+
from .fd_cpp import fd_cpp
2931

3032
__all__ = []
3133

3234
if not compas.IPY:
3335
__all__ += [
3436
'fd_numpy',
3537
'mesh_fd_numpy',
38+
'fd_cpp'
3639
]

src/compas_fd/fd/fd_cpp.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/compas_fd/fd/fd_cpp/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""
2+
******************
3+
compas_fd.fd.fd_cpp
4+
******************
5+
6+
.. currentmodule:: compas_fd.fd.fd_cpp
7+
8+
9+
Functions
10+
=========
11+
12+
.. autosummary::
13+
:toctree: generated/
14+
:nosignatures:
15+
16+
fd_cpp
17+
18+
"""
19+
from __future__ import print_function
20+
from __future__ import absolute_import
21+
from __future__ import division
22+
23+
import compas
24+
25+
if not compas.IPY:
26+
from .fd_cpp import fd_cpp
27+
28+
__all__ = []
29+
30+
if not compas.IPY:
31+
__all__ += ['fd_cpp']

src/compas_fd/fd/fd_cpp/docs/PLACEHOLDER

Whitespace-only changes.

src/compas_fd/fd/fd_cpp/fd_cpp.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import Optional, Sequence
2+
from typing_extensions import Annotated
3+
4+
from ..result import Result
5+
from .fdm import fd_solve # win64 build
6+
7+
8+
def fd_cpp(*,
9+
vertices: Sequence[Annotated[Sequence[float], 3]],
10+
fixed: Sequence[int],
11+
edges: Sequence[Annotated[Sequence[int], 2]],
12+
force_densities: Sequence[float],
13+
loads: Optional[Sequence[Annotated[Sequence[float], 3]]] = None
14+
) -> Result:
15+
"""Compute the equilibrium conditions by the force density method.
16+
The algorithms backend is implemented in C++ through pybind."""
17+
return Result(*fd_solve(vertices, fixed, edges, force_densities, loads))
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#ifndef COMPAS
2+
#define COMPAS
3+
4+
#include <pybind11/pybind11.h>
5+
#include <pybind11/eigen.h>
6+
#include <pybind11/stl.h>
7+
8+
#include <vector>
9+
#include <tuple>
10+
#include <numeric>
11+
#include <exception>
12+
#include <Eigen/Sparse>
13+
#include <Eigen/SparseCholesky>
14+
15+
16+
namespace py = pybind11;
17+
18+
using vi = std::vector<int>;
19+
using vd = std::vector<double>;
20+
using vvi = std::vector<std::vector<int>>;
21+
using vvd = std::vector<std::vector<double>>;
22+
using Md = Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic>;
23+
using Md3 = Eigen::Matrix<double, Eigen::Dynamic, 3>;
24+
using Md1 = Eigen::Vector<double, Eigen::Dynamic>;
25+
using DMd = Eigen::DiagonalMatrix<double, Eigen::Dynamic, Eigen::Dynamic>;
26+
using SMd = Eigen::SparseMatrix<double>;
27+
28+
29+
#endif // COMPAS
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#include "compas.h"
2+
#include "connectivity_matrices.h"
3+
4+
5+
//aliases
6+
using T = Eigen::Triplet<int>;
7+
8+
void
9+
set_connectivity_matrices(
10+
const vvi& edges,
11+
const vi& free_vertices,
12+
const vi& fixed_vertices,
13+
SMd& C, SMd& Ci, SMd& Cf)
14+
{
15+
std::vector<T> triplets, free_triplets, fixed_triplets;
16+
triplets.reserve(2 * edges.size());
17+
free_triplets.reserve(free_vertices.size());
18+
fixed_triplets.reserve(fixed_vertices.size());
19+
SMd Ci_mask(C.cols(), free_vertices.size());
20+
SMd Cf_mask(C.cols(), fixed_vertices.size());
21+
22+
// full connectivity matrix
23+
for (unsigned int i = 0; i < edges.size(); ++i)
24+
{
25+
triplets.emplace_back(i, edges[i][0], 1);
26+
triplets.emplace_back(i, edges[i][1], -1);
27+
}
28+
C.setFromTriplets(triplets.begin(), triplets.end());
29+
30+
// free vertices connectivity matrix
31+
for (unsigned int i = 0; i < free_vertices.size(); ++i)
32+
free_triplets.emplace_back(free_vertices[i], i, 1);
33+
Ci_mask.setFromTriplets(free_triplets.begin(), free_triplets.end());
34+
Ci = C * Ci_mask;
35+
36+
// fixed vertices connectivity matrix
37+
for (unsigned int i = 0; i < fixed_vertices.size(); ++i)
38+
fixed_triplets.emplace_back(fixed_vertices[i], i, 1);
39+
Cf_mask.setFromTriplets(fixed_triplets.begin(), fixed_triplets.end());
40+
Cf = C * Cf_mask;
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#ifndef CONNECTIVITY_MATRICES
2+
#define CONNECTIVITY_MATRICES
3+
4+
#include "compas.h"
5+
6+
7+
// Construct the connectivity matrices of a connected graph.
8+
// Coefficient (i, j) equals 1 if edge i starts at vertex i,
9+
// -1 if edge i ends at vertex j, and 0 otherwise.
10+
// Matrices are defined in-place as output parameters:
11+
// full matrix C, free vertices matrix Ci, fixed vertices matrix Cf.
12+
void
13+
set_connectivity_matrices(
14+
const vvi& edges,
15+
const vi& free_vertices,
16+
const vi& fixed_vertices,
17+
SMd& C, SMd& Ci, SMd& Cf);
18+
19+
20+
#endif // CONNECTIVITY_MATRICES
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#include "compas.h"
2+
#include "fd_solvers.h"
3+
#include "process_vertices.h"
4+
#include "connectivity_matrices.h"
5+
#include "linalg_conversion.h"
6+
7+
8+
std::tuple<Md3, Md3, Md1, Md1>
9+
fd_solve(
10+
vvd& vertex_coordinates,
11+
vi& fixed_vertices,
12+
vvi& edges,
13+
vd& force_densities,
14+
vvd& loads)
15+
{
16+
// pre-process vertex indices arrays
17+
int vertex_count = vertex_coordinates.size();
18+
int edge_count = edges.size();
19+
vi free_vertices;
20+
set_free_vertices(vertex_count, edge_count,
21+
fixed_vertices, free_vertices);
22+
23+
// set primary data matrices
24+
Md3 X = matrixX3_from_vec2d(vertex_coordinates);
25+
Md3 P = matrixX3_from_vec2d(loads);
26+
auto& q = matrix_from_vec1d(force_densities);
27+
DMd Q = q.asDiagonal();
28+
29+
// set connectivity matrices
30+
SMd C(edge_count, vertex_count);
31+
SMd Ci(edge_count, free_vertices.size());
32+
SMd Cf(edge_count, fixed_vertices.size());
33+
set_connectivity_matrices(edges, free_vertices, fixed_vertices, C, Ci, Cf);
34+
35+
// solve for current force densities
36+
// build stiffness matrices
37+
SMd Cit = Ci.transpose();
38+
SMd D = C.transpose() * Q * C;
39+
SMd Di = Cit * Q * Ci;
40+
SMd Df = Cit * Q * Cf;
41+
42+
// solve for free coordinates
43+
Md b = P(free_vertices, Eigen::all) - (Df * X(fixed_vertices, Eigen::all));
44+
Eigen::SimplicialLDLT<SMd> solver;
45+
solver.compute(Di);
46+
if (solver.info() != Eigen::Success)
47+
throw std::exception("Singular stiffness matrix");
48+
Md3 X_free = solver.solve(b); // updated free vertex coordinates
49+
for (unsigned int i = 0; i < X_free.rows(); ++i)
50+
X.row(free_vertices[i]) = X_free.row(i);
51+
52+
// get dependent variables
53+
Md3 R = P - (D * X); // residuals and reactions
54+
Md1 L = (C * X).rowwise().norm(); // edge lengths
55+
Md1 F = Q.diagonal().array() * L.array(); // edge forces
56+
57+
return {X, R, F, L};
58+
}
59+
60+
61+
void init_fd_solvers(py::module& m)
62+
{
63+
m.def("fd_solve",
64+
&fd_solve,
65+
py::arg("vertex_coordinates").noconvert(),
66+
py::arg("fixed_vertices").noconvert(),
67+
py::arg("edges").noconvert(),
68+
py::arg("force_densities").noconvert(),
69+
py::arg("loads").noconvert());
70+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#ifndef FD_SOLVERS
2+
#define FD_SOLVERS
3+
4+
5+
// One-shot equilibrium calculation by the force density method.
6+
std::tuple<Md3, Md3, Md1, Md1>
7+
fd_solve(
8+
vvd& vertex_coordinates,
9+
vi& fixed_vertices,
10+
vvi& edges,
11+
vd& force_densities,
12+
vvd& loads);
13+
14+
#endif // FD_SOLVERS

src/compas_fd/fd/fd_cpp/src/fdm.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#include "compas.h"
2+
#include "fd_solvers.h"
3+
4+
5+
void init_fd_solvers(py::module&);
6+
7+
PYBIND11_MODULE(fdm, m)
8+
{
9+
m.doc() = "force density algorithms using Eigen.";
10+
init_fd_solvers(m);
11+
}

0 commit comments

Comments
 (0)