修改表面代码以求解 4 个维度而不是 3 个 [编辑]

Modify surface code to solve for 4 dimensions instead of 3 [edited]

我通过一些简洁的代码发现了这个很好的问题,通过一些调整,将 3D 多项式曲面拟合到 space.

中的一组点上

Python 3D polynomial surface fit, order dependent

下面是我的版本

最终,我意识到随着时间的推移我需要拟合一个曲面,即我需要求解一个 4 维曲面,我一直在努力解决这个问题。

我想出了一个非常复杂且计算量大的解决方法。我为每个时间间隔创建一个表面。然后我创建一个点网格并找到每个表面上每个点的 Z 值。所以现在我有一堆 x,y 点,每个点都有一个 z 值列表,这些值需要从一个间隔平稳地流动到下一个间隔。所以我们对 z 值进行回归。现在 z 流是平滑的,我根据 x、y 点以及相关时间间隔的平滑 Z 值为每个时间间隔重新拟合表面。

听起来就是这样。笨拙且次优。生成的曲面流动得更顺畅并且仍然表现良好,但必须有一种方法可以去掉中间人并直接在 fitSurface 函数中求解第 4 维。

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import itertools

# Start black magic
def xy_powers(order):
    powers = itertools.product(range(order + 1), range(order + 1))
    return [tup for tup in powers if sum(tup) <= order]

def fitSurface(x, y, z, order):
    ncols = (order + 1)**2
    G = np.zeros((x.size, ncols))
    ij = xy_powers(order)
    for k, (i,j) in enumerate(ij):
        G[:,k] = x**i * y**j
    m, _, _, _ = np.linalg.lstsq(G, z, rcond=None)
    return m

def getZValuesForXYInputs(surface, order, x, y):
    order = int(np.sqrt(len(surface))) - 1
    ij = xy_powers(order)
    z = np.zeros_like(x)
    for a, (i,j) in zip(surface, ij):
        z += a * x**i * y**j
    return z
# End black magic

def showRender_3D(x_raw, y_raw, z_raw, xx, yy, zz):
    fig = plt.figure()
    ax = Axes3D(fig)
    ax.scatter(x_raw, y_raw, z_raw, color='red', zorder=0)
    ax.plot_surface(xx, yy, zz, zorder=10, alpha=0.4)
    ax.set_xlabel('X data')
    ax.set_ylabel('Y data')
    ax.set_zlabel('Z data')
    plt.show()



def main(order):
    # Make generic data
    numdata = 100
    x = np.random.random(numdata)
    y = np.random.random(numdata)
    z = x**2 + y**2 + 3*x**3 + y + np.random.random(numdata)
    t = np.random.randint(1, 4, numdata) # Break the data into 

    # Fit the surface
    m = fitSurface(x, y, z, order)

    # Sample the surface at regular points so we can more easily plot the surface
    nx, ny = 40, 40
    xx, yy = np.meshgrid(np.linspace(x.min(), x.max(), nx), 
                         np.linspace(y.min(), y.max(), ny))
    zz = getZValuesForXYInputs(m, order, xx, yy)

    # Plot it
    showRender_3D(x, y, z, xx, yy, zz)

orderForSurfaceFit = 3
main(orderForSurfaceFit)

好吧,我想我已经拨通了这个。我不会详细说明如何,只是说一旦你对代码进行了足够的研究,魔法就不会消失,但模式确实会出现。我只是扩展了这些模式,它看起来很有效。

最终结果

无可否认,这是如此低的分辨率,看起来它没有从 C=1 变为 C=2,但确实如此。加载它,你会看到。 gif 现在应该更清楚地显示流程了。


首先是证明背后的方法论。我找到了一个时髦的表面方程并添加了第三个输入变量 C(in-effect 创建 4D 表面),然后使用原始 3D fitter/renderer 作为可信参考点研究具有固定 C 值的表面形状。

当 C 为 1 时,你从地狱得到半管。一个稍微不平衡的向下倾斜的半管。

当 C 为 2 时,您得到的结果大致相同,但不平衡性更加夸张。

当 C 为 3 时,你会得到一个非常不同的形状。就像上面夸张的半管被切成两半,反转,然后粘在一起。


当你 运行 下面的代码时,你会得到一个带有滑块的 3D 渲染,它允许你从 1 到 3 流动 C 值。1、2 和 3 处的值看起来像实心与参考相匹配。我还向数据添加了一个随机化器,以查看它在从不完美的噪声数据近似表面时的表现如何,我也喜欢我在那里看到的结果。

支持以下问题的代码和想法。

Python 3D polynomial surface fit, order dependent

import itertools
import numpy as np
import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D
from matplotlib.widgets import Slider


class Surface4D:
    def __init__(self, order, a, b, c, z):
        # Setting initial attributes
        self.order = order
        self.a = a
        self.b = b
        self.c = c
        self.z = z
        # Setting surface attributes
        self.surface = self._fit_surface()
        self.aa = None
        self.bb = None
        self._sample_surface_grid()
        # Setting graph attributes
        self.surface_render = None
        self.axis_3d = None

    # Start black magic math
    def _abc_powers(self):
        powers = itertools.product(range(self.order + 1), range(self.order + 1), range(self.order + 1))
        return [tup for tup in powers if sum(tup) <= self.order]

    def _fit_surface(self):
        ncols = (self.order + 1)**3
        G = np.zeros((self.a.size, ncols))
        ijk = self._abc_powers()
        for idx, (i,j,k) in enumerate(ijk):
            G[:,idx] = self.a**i * self.b**j * self.c**k
        m, _, _, _ = np.linalg.lstsq(G, self.z, rcond=None)
        return m

    def get_z_values(self, a, b, c):
        ijk = self._abc_powers()
        z = np.zeros_like(a)
        for s, (i,j,k) in zip(self.surface, ijk):
            z += s * a**i * b**j * c**k
        return z
    # End black magic math

    def render_4d_flow(self):
        # Set up the layout of the graph
        fig = plt.figure()
        self.axis_3d = Axes3D(fig, rect=[0.1,0.2,0.8,0.7])
        slider_ax = fig.add_axes([0.1,0.1,0.8,0.05])
        self.axis_3d.set_xlabel('X data')
        self.axis_3d.set_ylabel('Y data')
        self.axis_3d.set_zlabel('Z data')

        # Plot the point cloud and initial surface
        self.axis_3d.scatter(self.a, self.b, self.z, color='red', zorder=0)
        zz = self.get_z_values(self.aa, self.bb, 1)
        self.surface_render = self.axis_3d.plot_surface(self.aa, self.bb, zz, zorder=10, alpha=0.4, color="b")

        # Setup the slider behavior
        slider_start_step = self.c.min()
        slider_max_steps = self.c.max()
        slider = Slider(slider_ax, 'time', slider_start_step, slider_max_steps , valinit=slider_start_step)
        slider.on_changed(self._update)

        plt.show()
        input("Once youre done, hit any enter to continue.")

    def _update(self, val):
        self.surface_render.remove()
        zz = self.get_z_values(self.aa, self.bb, val)
        self.surface_render = self.axis_3d.plot_surface(self.aa, self.bb, zz, zorder=10, alpha=0.4, color="b")

    def _sample_surface_grid(self):
        na, nb = 40, 40
        aa, bb = np.meshgrid(np.linspace(self.a.min(), self.a.max(), na), 
                            np.linspace(self.b.min(), self.b.max(), nb))
        self.aa = aa
        self.bb = bb

def noisify_array(one_dim_array, randomness_multiplier):
    listOfNewValues = []
    range = abs(one_dim_array.min()-one_dim_array.max())
    for item in one_dim_array:
        # What percentage are we shifting the point dimension by
        shift = np.random.randint(0, 101)
        shiftPercent = shift/100
        shiftPercent = shiftPercent * randomness_multiplier

        # Is that shift positive or negative
        shiftSignIndex = np.random.randint(0, 2)
        shifSignOption = [-1, 1]
        shiftSign = shifSignOption[shiftSignIndex]

        # Shift it
        newNoisyPosition = item + (range * (shiftPercent * shiftSign))
        listOfNewValues.append(newNoisyPosition)
    return np.array(listOfNewValues)


def generate_data():
    # Define our range for each dimension
    x = np.linspace(-6, 6, 20)
    y = np.linspace(-6, 6, 20)
    w = [1, 2, 3]

    # Populate each dimension
    a,b,c,z = [],[],[],[]
    for X in x:
        for Y in y:
            for W in w:
                a.append(X)
                b.append(Y)
                c.append(W)
                z.append((1 * X ** 4) + (2 * Y ** 3) + (X * Y ** W) + (4 * X) + (5 * Y))

    # Convert them to arrays
    a = np.array(a)
    b = np.array(b)
    c = np.array(c)
    z = np.array(z)

    return [a, b, c, z]

def main(order):
    # Make the data
    a,b,c,z = generate_data()

    # Show the pure data and best fit
    surface_pure_data = Surface4D(order, a, b, c, z)
    surface_pure_data.render_4d_flow()

    # Add some noise to the data
    a = noisify_array(a, 0.10)
    b = noisify_array(b, 0.10)
    c = noisify_array(c, 0.10)
    z = noisify_array(z, 0.10)

    # Show the noisy data and best fit
    surface_noisy_data = Surface4D(order, a, b, c, z)
    surface_noisy_data.render_4d_flow()


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

orderForSurfaceFit = 5
main(orderForSurfaceFit)

[编辑 1:我已经开始将此代码合并到我的实际项目中,并且我发现了一些调整以使事情变得合理。还有一个范围问题,运行time 需要在它仍在 render_4d_flow 函数的范围内时暂停,以便滑块工作。]

[编辑 2:显示从 c=2 到 c=3 的流程的高分辨率 gif]