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, |
这意味着实际上对于
假设我们采用一般点
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 |
其中
您可以使用您的函数并绘制它的等值面,但这不是最理想的:您需要为您的函数进行网格近似,这对于足够的分辨率来说非常昂贵,并且您必须为此采样选择域明智的。
相反,您可以对数据执行主轴转换,以概括您自己链接的规范椭球的参数图。
第一步是将
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 |
激发了辅助坐标
1 | r1 @ D @ r1 + b1 @ r1 + c0 == 0. |
由于
1 | d1 * x1**2 + d2 * x2**2 + d3 * x3**3 + b11 * x1 + b12 * x2 + b13 * x3 + c0 == 0 |
其中
剩下的就是从
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 |
这是二阶曲面的规范形式。为了使它有意义地对应于一个椭球,我们必须确保
这就是我们要做的:
以下是我的实现方式:
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() |
结果如下:
围绕它旋转很明显,表面确实相对于
您可以更改函数的
您还可以使用其他库来可视化曲面,例如 mayavi,您可以在其中绘制我们刚刚计算的曲面,或者将其与内置的等值面进行比较。