Skeletonization using Proxies¶
Copyright (C) 2020 Andreas Kloeckner
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import numpy as np
import numpy.linalg as la
import matplotlib.pyplot as pt
import scipy.linalg.interpolative as sli
def interp_decomp(A, k):
idx, proj = sli.interp_decomp(A, k)
P = np.hstack([np.eye(k), proj])[:,np.argsort(idx)]
return P, idx[:k]
eps = 1e-7
sources = np.random.rand(2, 200)
targets = np.random.rand(2, 200) + 3
pt.plot(sources[0], sources[1], "go")
pt.plot(targets[0], targets[1], "ro")
pt.xlim([-1, 5])
pt.ylim([-1, 5])
pt.gca().set_aspect("equal")
def interaction_mat(t, s):
all_distvecs = s.reshape(2, 1, -1) - t.reshape(2, -1, 1)
dists = np.sqrt(np.sum(all_distvecs**2, axis=0))
return np.log(dists)
def numerical_rank(A, eps):
_, sigma, _ = la.svd(A)
return np.sum(sigma >= eps)
Check the interaction rank:
numerical_rank(interaction_mat(targets, sources), eps)
np.int64(9)
Idea:
- Don't want to build whole matrix to find the few rows/columns that actually matter.
- Introduces "proxies" that stand in for
- all sources outside the targets or
- all targets outside these sources
Target Skeletonization¶
nproxies = 25
angles = np.linspace(0, 2*np.pi, nproxies)
target_proxies = 3.5 + 1.5 * np.array([np.cos(angles), np.sin(angles)])
pt.plot(sources[0], sources[1], "go")
pt.plot(targets[0], targets[1], "ro")
pt.plot(target_proxies[0], target_proxies[1], "bo")
pt.xlim([-1, 5])
pt.ylim([-1, 5])
pt.gca().set_aspect("equal")
Construct the interaction matrix from the target proxies to the targets as target_proxy_mat.
A note on terminology: The target_proxies are near the targets but stand in for far-away sources.
target_proxy_mat = interaction_mat(targets, target_proxies)
Check its numerical rank and shape:
numerical_rank(target_proxy_mat, eps)
np.int64(24)
target_proxy_mat.shape
(200, 25)
Now compute an ID (row or column?):
P, target_skeleton = interp_decomp(target_proxy_mat.T, nproxies)
Check that the ID does what is promises:
tpm_approx = P.T @ target_proxy_mat[target_skeleton]
la.norm(tpm_approx - target_proxy_mat, 2)
np.float64(4.318300865373527e-15)
Plot the chosen "skeleton" and the proxies:
pt.plot(sources[0], sources[1], "go")
pt.plot(targets[0], targets[1], "ro", alpha=0.05)
pt.plot(targets[0, target_skeleton], targets[1, target_skeleton], "ro")
pt.plot(target_proxies[0], target_proxies[1], "bo")
pt.xlim([-1, 5])
pt.ylim([-1, 5])
pt.gca().set_aspect("equal")
What does this mean?
- We have now got a moral equivalent to a local expansion: The point values at the target skeleton points.
- Is it a coincidence that the skeleton points sit at the boundary of the target region?
- How many target proxies should we choose?
- Can cheaply recompute potential at any target from those few points.
- Have thus reduce LA-based evaluation cost to same as expansion-based cost.
Can we come up with an equivalent of a multipole expansion?
Check that this works for 'our' sources:
imat_error = (
P.T @ interaction_mat(targets[:, target_skeleton], sources)
-
interaction_mat(targets, sources))
la.norm(imat_error, 2)
np.float64(5.426832317627278e-09)
Source Skeletonization¶
nproxies = 25
angles = np.linspace(0, 2*np.pi, nproxies)
source_proxies = 0.5 + 1.5 * np.array([np.cos(angles), np.sin(angles)])
pt.plot(sources[0], sources[1], "go")
pt.plot(targets[0], targets[1], "ro")
pt.plot(source_proxies[0], source_proxies[1], "bo")
pt.xlim([-1, 5])
pt.ylim([-1, 5])
pt.gca().set_aspect("equal")
Construct the interaction matrix from the sources to the source proxies as source_proxy_mat:
A note on terminology: The source_proxies are near the sources but stand in for far-away targets.
source_proxy_mat = interaction_mat(source_proxies, sources)
source_proxy_mat.shape
(25, 200)
Now compute an ID (row or column?):
P, source_skeleton = interp_decomp(source_proxy_mat, nproxies)
tsm_approx = source_proxy_mat[:, source_skeleton] @ P
la.norm(tsm_approx - source_proxy_mat, 2)
np.float64(3.471111818432676e-15)
Plot the chosen skeleton as well as the proxies:
pt.plot(sources[0], sources[1], "go", alpha=0.05)
pt.plot(targets[0], targets[1], "ro")
pt.plot(sources[0, source_skeleton], sources[1, source_skeleton], "go")
pt.plot(source_proxies[0], source_proxies[1], "bo")
pt.xlim([-1, 5])
pt.ylim([-1, 5])
pt.gca().set_aspect("equal")
Check that it works for 'our' targets:
imat_error = (
interaction_mat(targets, sources[:, source_skeleton]) @ P
-
interaction_mat(targets, sources))
la.norm(imat_error, 2)
np.float64(5.7679856249083905e-09)
- Sensibly, this is just the transpose of the target skeletonization process.
- For a given point cluster, the same skeleton can serve for target and source skeletonization!
- Computationally, starting from your original charges $x$, you accumulate 'new' charges $Px$ at the skeleton points and then only compute the interaction from the source skeleton to the targets.
Hierarchical Skeletonization¶
gathered_skeletons = np.concatenate(
[targets[:, target_skeleton],
targets[:, target_skeleton] + np.array([0, 1]).reshape(-1, 1),
targets[:, target_skeleton] + np.array([1, 0]).reshape(-1, 1),
targets[:, target_skeleton] + np.array([1, 1]).reshape(-1, 1),
], axis=1)
pt.plot(gathered_skeletons[0], gathered_skeletons[1], "ro")
[<matplotlib.lines.Line2D at 0x7f23a29f3620>]
target_proxies = 4 + 2 * np.array([np.cos(angles), np.sin(angles)])
pt.plot(target_proxies[0], target_proxies[1], "bo")
parent_proxy_mat = interaction_mat(gathered_skeletons, target_proxies)
idx, proj = sli.interp_decomp(parent_proxy_mat.T, nproxies)
parent_skeleton = idx[:nproxies]
pt.plot(gathered_skeletons[0, parent_skeleton], gathered_skeletons[1, parent_skeleton], "ro")
[<matplotlib.lines.Line2D at 0x7f23a4a9f380>]