Improving a numpy implementation of a simple spring network

岁酱吖の 提交于 2019-12-05 08:08:12

问题


I wanted a very simple spring system written in numpy. The system would be defined as a simple network of knots, linked by links. I'm not interested in evaluating the system over time, but instead I want to go from an initial state, change a variable (usually move a knot to a new position) and solve the system until it reaches a stable state (last applied force is below a given threshold). The knots have no mass, there's no gravity, the forces are all derived from each link's current lengths/init lengths. And the only "special" variable is that each knot can bet set as "anchored" (doesn't move).

So I wrote this simple solver below, and included a simple example. Jump to the very end for my question.

import numpy as np
from numpy.core.umath_tests import inner1d

np.set_printoptions(precision=4)
np.set_printoptions(suppress=True)
np.set_printoptions(linewidth =150)
np.set_printoptions(threshold=10)


def solver(kPos, kAnchor, link0, link1, w0, cycles=1000, precision=0.001, dampening=0.1, debug=False):
    """
    kPos       : vector array - knot position
    kAnchor    : float array  - knot's anchor state, 0 = moves freely, 1 = anchored (not moving)
    link0      : int array    - array of links connecting each knot. each index corresponds to a knot
    link1      : int array    - array of links connecting each knot. each index corresponds to a knot
    w0         : float array  - initial link length
    cycles     : int          - eval stops when n cycles reached
    precision  : float        - eval stops when highest applied force is below this value
    dampening : float        - keeps system stable during each iteration
    """

    kPos        = np.asarray(kPos)
    pos         = np.array(kPos) # copy of kPos
    kAnchor     = 1-np.clip(np.asarray(kAnchor).astype(float),0,1)[:,None]
    link0       = np.asarray(link0).astype(int)
    link1       = np.asarray(link1).astype(int)
    w0          = np.asarray(w0).astype(float)

    F = np.zeros(pos.shape)
    i = 0

    for i in xrange(cycles):

        # Init force applied per knot
        F = np.zeros(pos.shape)

        # Calculate forces
        AB = pos[link1] - pos[link0] # get link vectors between knots
        w1 = np.sqrt(inner1d(AB,AB)) # get link lengths
        AB/=w1[:,None] # normalize link vectors
        f = (w1 - w0) # calculate force vectors
        f = f[:,None] * AB

        # Apply force vectors on each knot
        np.add.at(F, link0, f)
        np.subtract.at(F, link1, f)

        # Update point positions       
        pos += F * dampening * kAnchor

        # If the maximum force applied is below our precision criteria, exit
        if np.amax(F) < precision:
            break

    # Debug info
    if debug:
        print 'Iterations: %s'%i
        print 'Max Force:  %s'%np.amax(F)

    return pos

Here's some test data to show how it works. In this case i'm using a grid, but in reality this can be any type of network, like a string with many knots, or a mess of polygons...:

import cProfile

# Create a 5x5 3D knot grid
z = np.linspace(-0.5, 0.5, 5)
x = np.linspace(-0.5, 0.5, 5)[::-1]
x,z = np.meshgrid(x,z)
kPos = np.array([np.array(thing) for thing in zip(x.flatten(), z.flatten())])
kPos = np.insert(kPos, 1, 0, axis=1)
'''
array([[-0.5 ,  0.  ,  0.5 ],
       [-0.25,  0.  ,  0.5 ],
       [ 0.  ,  0.  ,  0.5 ],
       ..., 
       [ 0.  ,  0.  , -0.5 ],
       [ 0.25,  0.  , -0.5 ],
       [ 0.5 ,  0.  , -0.5 ]])
'''


# Define the links connecting each knots
link0 = [0,1,2,3,5,6,7,8,10,11,12,13,15,16,17,18,20,21,22,23,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]
link1 = [1,2,3,4,6,7,8,9,11,12,13,14,16,17,18,19,21,22,23,24,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]
AB    = kPos[link0]-kPos[link1]
w0    = np.sqrt(inner1d(AB,AB)) # this is a square grid, each link's initial length will be 0.25

# Set the anchor states
kAnchor = np.zeros(len(kPos)) # All knots will be free floating
kAnchor[12] = 1 # Middle knot will be anchored

This is what the grid looks like:

If we run my code using this data, nothing will happen since the links aren't pushing or stretching:

print np.allclose(kPos,solver(kPos, kAnchor, link0, link1, w0, debug=True))
# Returns True
# Iterations: 0
# Max Force:  0.0

Now lets move that middle anchored knot up a bit and solve the system:

# Move the center knot up a little
kPos[12] = np.array([0,0.3,0])

# eval the system
new = solver(kPos, kAnchor, link0, link1, w0, debug=True) # positions will have moved
#Iterations: 102
#Max Force:  0.000976603249133

# Rerun with cProfile to see how fast it runs
cProfile.run('solver(kPos, kAnchor, link0, link1, w0)')
# 520 function calls in 0.008 seconds

And here's what the grid looks like after being pulled by that single anchored knot:

Question:

My actual use cases are a little more complex than this example and solve a little too slow for my taste: (100-200 knots with a network anywhere between 200-300 links, solves in a few seconds).

How can i make my solver function run faster? I'd consider Cython but i have zero experience with C. Any help would be greatly appreciated.


回答1:


Your method, at a cursory glance, appears to be an explicit under-relaxation type of method. Calculate the residual force at each knot, apply a factor of that force as a displacement, repeat until convergence. It's the repeating until convergence that takes the time. The more points you have, the longer each iteration takes, but you also need more iterations for the constraints at one end of the mesh to propagate to the other.

Have you considered an implicit method? Write the equation for the residual force at each non-constrained node, assemble them into a large matrix, and solve in one step. Information now propagates across the entire problem in a single step. As an additional benefit, the matrix you construct should be sparse, which scipy has a module for.

Wikipedia: explicit and implicit methods


EDIT Example of an implicit method matching (roughly) your problem. This solution is linear, so it doesn't take into account the effect of the calculated displacement on the force. You would need to iterate (or use non-linear techniques) to calculate this. Hope it helps.

#!/usr/bin/python3

import matplotlib.pyplot as pp
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import scipy as sp
import scipy.sparse
import scipy.sparse.linalg

#------------------------------------------------------------------------------#

# Generate a grid of knots
nX = 10
nY = 10
x = np.linspace(-0.5, 0.5, nX)
y = np.linspace(-0.5, 0.5, nY)
x, y = np.meshgrid(x, y)
knots = list(zip(x.flatten(), y.flatten()))

# Create links between the knots
links = []
# Horizontal links
for i in range(0, nY):
    for j in range(0, nX - 1):
        links.append((i*nX + j, i*nX + j + 1))
# Vertical links
for i in range(0, nY - 1):
    for j in range(0, nX):
        links.append((i*nX + j, (i + 1)*nX + j))

# Create constraints. This dict takes a knot index as a key and returns the
# fixed z-displacement associated with that knot.
constraints = {
    0          : 0.0,
    nX - 1     : 0.0,
    nX*(nY - 1): 0.0,
    nX*nY - 1  : 1.0,
    2*nX + 4   : 1.0,
    }

#------------------------------------------------------------------------------#

# Matrix i-coordinate, j-coordinate and value
Ai = []
Aj = []
Ax = []

# Right hand side array
B = np.zeros(len(knots))

# Loop over the links
for link in links:

    # Link geometry
    displacement = np.array([ knots[1][i] - knots[0][i] for i in range(2) ])
    distance = np.sqrt(displacement.dot(displacement))

    # For each node
    for i in range(2):

        # If it is not a constraint, add the force associated with the link to
        # the equation of the knot
        if link[i] not in constraints:

            Ai.append(link[i])
            Aj.append(link[i])
            Ax.append(-1/distance)

            Ai.append(link[i])
            Aj.append(link[not i])
            Ax.append(+1/distance)

        # If it is a constraint add a diagonal and a value
        else:

            Ai.append(link[i])
            Aj.append(link[i])
            Ax.append(+1.0)
            B[link[i]] += constraints[link[i]]

# Create the matrix and solve
A = sp.sparse.coo_matrix((Ax, (Ai, Aj))).tocsr()
X = sp.sparse.linalg.lsqr(A, B)[0]

#------------------------------------------------------------------------------#

# Plot the links
fg = pp.figure()
ax = fg.add_subplot(111, projection='3d')

for link in links:
    x = [ knots[i][0] for i in link ]
    y = [ knots[i][1] for i in link ]
    z = [ X[i] for i in link ]
    ax.plot(x, y, z)

pp.show()


来源:https://stackoverflow.com/questions/35520384/improving-a-numpy-implementation-of-a-simple-spring-network

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!