pythonpython-3.xmatplotlib

How to draw a ribbon wrapped around a sphere in matplotlib?


I am trying to plot a ribbon wrapped around a sphere but without much success

The result should look like this, it is basically the part of the surface of a sphere but only the ribbon part.

ribbon wrapped around a sphere

The code that I wrote:

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

# Funzione per convertire le coordinate sferiche in coordinate cartesiane
# Le coordinate sferiche ci permettono di rappresentare la sfera in modo più semplice
def sferiche_a_cartesiane(r, theta, phi): # r raggio che varia, theta è la colatitudine
    x = r * np.sin(phi) * np.cos(theta)
    y = r * np.sin(phi) * np.sin(theta)
    z = r * np.cos(phi)
    return x, y, z

# Genera le coordinate sferiche per la sfera usando un numpy array di x numeri equidistanti
theta_sfera = np.linspace(0, 2 * np.pi, 100)
phi_sfera = np.linspace(0, np.pi, 50)
theta_sfera, phi_sfera = np.meshgrid(theta_sfera, phi_sfera) # coordinate vere e proprie

# Imposta il raggio della sfera
raggio_sfera = 1.0 

# Calcola le coordinate cartesiane dalla sferiche
x_sfera, y_sfera, z_sfera = sferiche_a_cartesiane(raggio_sfera, theta_sfera, phi_sfera)

# Crea la figura e gli assi 3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

# Disegna la sfera
ax.plot_surface(x_sfera, y_sfera, z_sfera, color='r', alpha=0.1, edgecolors=None, zorder=0)

t = np.linspace(-1, 1, 1000) # t appartiene a [-1, 1]
# Genera la curva parametrica con raggio leggermente ingrandito. Si derivano dal fatto che la sfera ha eq. x**2 + y**2 + z**2 = 1
x_curva = 1.01 * ((1 - t**2)**0.5) * np.cos(10 * np.pi * t)
y_curva = 1.01 * ((1 - t**2)**0.5) * np.sin(10 * np.pi * t)
z_curva = 1 * t

# Ruota la curva parametrica attorno alla sfera
angolo_rotazione = np.pi / 4 
x_rotato = x_curva * np.cos(angolo_rotazione) - y_curva * np.sin(angolo_rotazione)
y_rotato = x_curva * np.sin(angolo_rotazione) + y_curva * np.cos(angolo_rotazione)
z_rotato = z_curva

# Disegna la curva parametrica ruotata
ax.plot(x_rotato, y_rotato, z_rotato, 'y', linewidth=2, zorder=1)

# Imposta gli assi
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')

# Mostra il grafico
plt.show()

This is the current output:

attempted ribbon on sphere

Anyone can help me?


Solution

  • I looked at your code, but I was not figure out how to coerce it into looking like the image you provided. However, I was able to generate a very similar image using the code shown below. This was primarily based on this question which covers generating and drawing those triangles. This code is also pretty amenable to changing the ribbon to include more or fewer loops around, or making it thinner or thicker, so it should adaptable to your needs.

    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d.art3d import Poly3DCollection
    import numpy as np
    
    
    def poltocart(r, theta, phi):
        # Clamp theta so that our slices don't stick out weirdly near the poles
        theta = max(min(theta, np.pi), 0)
        x = r * np.sin(theta) * np.cos(phi)
        y = r * np.sin(theta) * np.sin(phi)
        z = r * np.cos(theta)
        return x, y, z
    
    
    def _main():
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
    
        # Based on the provided image it looks like goes around
        #   maybe 5 times?
        turns = 5
        # How tightly to quantize the ribbon (100 has visible artifacts)
        phi_slices = 500
        # How thick each slice is (large = fatter ribbon)
        slice_thick = np.deg2rad(20)
    
        # The list of triangles to draw
        ribbon = []
        # How many times around to go based on turns
        max_phi = turns * 2 * np.pi
        # Generate all values of phi so that we can iterate through in pairs
        phi_values = list(np.linspace(-np.pi, max_phi, phi_slices, endpoint=True))
        for i in range(1, phi_slices - 1):
            # The previous slice (so that edges align)
            phix_minus1 = phi_values[i - 1]
            # The current slice
            phix0 = phi_values[i]
            # Top edge triangle
            p1 = poltocart(1, np.pi * phix_minus1 / max_phi, phix_minus1)
            p2 = poltocart(1, np.pi * phix0 / max_phi + slice_thick, phix_minus1)
            p3 = poltocart(1, np.pi * phix0 / max_phi, phix0)
            # Bottom edge triangle
            p1r = poltocart(1, np.pi * phix0 / max_phi + slice_thick + np.pi / phi_slices, phix0)
            p2r = poltocart(1, np.pi * phix0 / max_phi + slice_thick, phix_minus1)  # same as top
            p3r = poltocart(1, np.pi * phix0 / max_phi, phix0)  # same as top
            ribbon.append((p1, p2, p3))
            ribbon.append((p1r, p2r, p3r))
    
        p31 = Poly3DCollection(ribbon)
        p31.set(color=(1, 0, 0, 0.25))
        p31.set(ec=(1, 0, 0, 0.0))
        ax.add_collection(p31)
        ax.set_xlim([-1, 1])
        ax.set_ylim([-1, 1])
        ax.set_zlim([-1, 1])
        ax.set_axis_off()
        plt.show()
    
    
    if __name__ == '__main__':
        _main()
    

    And here is the output it generates: ribbon wrapped around sphere in 5 turns

    Let me know if you have any questions.