关于 python:如何在由二次方程定义的椭圆体的 matplotlib 中绘制 3D 图?

How can I make a 3D plot in matplotlib of an ellipsoid defined by a quadratic equation?

我有一个椭球的一般公式:

1
A*x**2 + C*y**2 + D*x + E*y + B*x*y + F + G*z**2 = 0

其中 A、B、C、D、E、F、G 是常数因子。

如何在 matplotlib 中将此方程绘制为 3D 图? (线框最好。)

我看到了这个例子,但它是参数形式,我不知道如何在这段代码中放置 z 坐标。有没有办法在没有参数形式的情况下保持一般形式来绘制这个?

我开始把它写成这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from mpl_toolkits import mplot3d
%matplotlib notebook
import numpy as np
import matplotlib.pyplot as plt

def f(x, y):
    return ((A*x**2 + C*y**2 + D*x + E*y + B*x*y + F))

def f(z):
    return G*z**2

x = np.linspace(-2200, 1850, 30)
y = np.linspace(-100, 60, 30)
z = np.linspace(-100, 60, 30)

X, Y, Z = np.meshgrid(x, y, z)

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z');

我收到此错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-1-95b1296ae6a4> in <module>()
     18 fig = plt.figure()
     19 ax = fig.add_subplot(111, projection='3d')
---> 20 ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
     21 ax.set_xlabel('x')
     22 ax.set_ylabel('y')

C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\Anaconda3_64\\lib\\site-packages\\mpl_toolkits\\mplot3d\\axes3d.py in plot_wireframe(self, X, Y, Z, *args, **kwargs)
   1847         had_data = self.has_data()
   1848         if Z.ndim != 2:
-> 1849             raise ValueError("Argument Z must be 2-dimensional.")
   1850         # FIXME: Support masked arrays
   1851         X, Y, Z = np.broadcast_arrays(X, Y, Z)

ValueError: Argument Z must be 2-dimensional.

旁注,但您所拥有的并不是 3d 椭球的最一般方程。你的方程可以改写为

1
A*x**2 + C*y**2 + D*x + E*y + B*x*y = - G*z**2 - F,

这意味着实际上对于 z 的每个值,您都会得到不同级别的 2d 椭圆,并且切片相对于 z = 0 平面对称。这表明您的椭球体不一般,它有助于检查结果以确保我们得到的结果是有意义的。

假设我们采用一般点r0 = [x0, y0, z0],你有

1
r0 @ M @ r0 + b0 @ r0 + c0 == 0

在哪里

1
2
3
4
5
M = [ A    B/2    0
     B/2    C     0
      0     0     G],
b0 = [D, E, 0],
c0 = F

其中 @ 代表矩阵向量或向量向量积。

您可以使用您的函数并绘制它的等值面,但这不是最理想的:您需要为您的函数进行网格近似,这对于足够的分辨率来说非常昂贵,并且您必须为此采样选择域明智的。

相反,您可以对数据执行主轴转换,以概括您自己链接的规范椭球的参数图。

第一步是将M对角化为M = V @ D @ V.T,其中D是对角线。因为它是一个实对称矩阵,所以这总是可能的,并且 V 是正交的。然后我们有

1
r0 @ V @ D @ V.T @ r0 + b0 @ r0 + c0 == 0

我们可以重新组合成

1
(V.T @ r0) @ D @ (V.T @ r0) + b0 @ V @ (V.T @ r0) + c0 == 0

激发了辅助坐标r1 = V.T @ r0和向量b1 = b0 @ V的定义,我们得到

1
r1 @ D @ r1 + b1 @ r1 + c0 == 0.

由于 D 是一个对称矩阵,其特征值 d1, d2, d3 在它的对角线上,所以上面的方程是

1
d1 * x1**2 + d2 * x2**2 + d3 * x3**3 + b11 * x1 + b12 * x2 + b13 * x3 + c0 == 0

其中 r1 = [x1, x2, x3]b1 = [b11, b12, b13].

剩下的就是从 r1 切换到 r2 以便我们删除线性项:

1
d1 * (x1 + b11/(2*d1))**2 + d2 * (x2 + b12/(2*d2))**2 + d3 * (x3 + b13/(2*d3))**2 - b11**2/(4*d1) - b12**2/(4*d2) - b13**2/(4*d3) + c0 == 0

所以我们定义

1
2
3
4
5
r2 = [x2, y2, z2]
x2 = x1 + b11/(2*d1)
y2 = y1 + b12/(2*d2)
z2 = z1 + b13/(2*d3)
c2 = b11**2/(4*d1) b12**2/(4*d2) b13**2/(4*d3) - c0.

这些我们终于有了

1
2
d1 * x2**2 + d2 * y2**2 + d3 * z2**2 == c2,
d1/c2 * x2**2 + d2/c2 * y2**2 + d3/c2 * z2**2 == 1

这是二阶曲面的规范形式。为了使它有意义地对应于一个椭球,我们必须确保 d1d2d3c2 都是严格正的。如果这得到保证,那么规范形式的半长轴是 sqrt(c2/d1)sqrt(c2/d2)sqrt(c2/d3)

这就是我们要做的:

  • 确保参数对应于椭球体
  • 为极角和方位角生成 theta 和 phi 网格
  • 计算变换后的坐标 [x2, y2, z2]
  • 将它们移回(通过 r2 - r1)以获得 [x1, y1, z1]
  • 将坐标转换回 V 以获得 r0,即我们感兴趣的实际 [x, y, z] 坐标。
  • 以下是我的实现方式:

    1
    2
    3
    4
    5
    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
    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D

    def get_transforms(A, B, C, D, E, F, G):
       """ Get transformation matrix and shift for a 3d ellipsoid

        Assume A*x**2 + C*y**2 + D*x + E*y + B*x*y + F + G*z**2 = 0,
        use principal axis transformation and verify that the inputs
        correspond to an ellipsoid.

        Returns: (d, V, s) tuple of arrays
            d: shape (3,) of semi-major axes in the canonical form
               (X/d1)**2 + (Y/d2)**2 + (Z/d3)**2 = 1
            V: shape (3,3) of the eigensystem
            s: shape (3,) shift from the linear terms
       """


        # construct original matrix
        M = np.array([[A, B/2, 0],
                      [B/2, C, 0],
                      [0, 0, G]])
        # construct original linear coefficient vector
        b0 = np.array([D, E, 0])
        # constant term
        c0 = F

        # compute eigensystem
        D, V = np.linalg.eig(M)
        if (D <= 0).any():
            raise ValueError("Parameter matrix is not positive definite!")

        # transform the shift
        b1 = b0 @ V

        # compute the final shift vector
        s = b1 / (2 * D)

        # compute the final constant term, also has to be positive
        c2 = (b1**2 / (4 * D)).sum() - c0
        if c2 <= 0:
            print(b1, D, c0, c2)
            raise ValueError("Constant in the canonical form is not positive!")

        # compute the semi-major axes
        d = np.sqrt(c2 / D)

        return d, V, s

    def get_ellipsoid_coordinates(A, B, C, D, E, F, G, n_theta=20, n_phi=40):
       """Compute coordinates of an ellipsoid on an ellipsoidal grid

        Returns: x, y, z arrays of shape (n_theta, n_phi)
       """


        # get canonical grid
        theta,phi = np.mgrid[0:np.pi:n_theta*1j, 0:2*np.pi:n_phi*1j]
        r2 = np.array([np.sin(theta) * np.cos(phi),
                       np.sin(theta) * np.sin(phi),
                       np.cos(theta)]) # shape (3, n_theta, n_phi)

        # get transformation data
        d, V, s = get_transforms(A, B, C, D, E, F, G)  # could be *args I guess

        # shift and transform back the coordinates
        r1 = d[:,None,None]*r2 - s[:,None,None]  # broadcast along first of three axes
        r0 = (V @ r1.reshape(3, -1)).reshape(r1.shape)  # shape (3, n_theta, n_phi)

        return r0  # unpackable to x, y, z of shape (n_theta, n_phi)

    这是一个带有椭圆体的示例并证明它可以工作:

    1
    2
    3
    A,B,C,D,E,F,G = args = 2, -1, 2, 3, -4, -3, 4
    x,y,z = get_ellipsoid_coordinates(*args)
    print(np.allclose(A*x**2 + C*y**2 + D*x + E*y + B*x*y + F + G*z**2, 0))  # True

    从这里开始的实际绘图是微不足道的。使用此答案中的 3d 缩放技巧来保留相等的轴:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # create 3d axes
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')

    # plot the data
    ax.plot_wireframe(x, y, z)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_zlabel('z')

    # scaling hack
    bbox_min = np.min([x, y, z])
    bbox_max = np.max([x, y, z])
    ax.auto_scale_xyz([bbox_min, bbox_max], [bbox_min, bbox_max], [bbox_min, bbox_max])

    plt.show()

    结果如下:
    figure

    您还可以使用其他库来可视化曲面,例如 mayavi,您可以在其中绘制我们刚刚计算的曲面,或者将其与内置的等值面进行比较。