2

I'm plotting multiple lines on the same graph using matplotlib in Python by using a for-loop to add each line to the axis.

When plotted in 2D with each line on top of the other this works fine.

When plotting in 3D however, python displays the same graphed data each time I run through the for-loop, even though the data is significantly different.

Edit: I don't believe that this question is a duplicate of "How can I tell if NumPy creates a view or a copy?" as it highlights one particular instance of unexpected behaviour.

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

###### Unimportant maths not relevant to the question ######

def rossler(x_n, y_n, z_n, h, a, b, c):
    #defining the rossler function
    x_n1=x_n+h*(-y_n-z_n)
    y_n1=y_n+h*(x_n+a*y_n)
    z_n1=z_n+h*(b+z_n*(x_n-c))   
    return x_n1,y_n1,z_n1

#defining a, b, and c
a = 1.0/5.0
b = 1.0/5.0
c = 5

#defining time limits and steps
t_0 = 0
t_f = 50*np.pi
h = 0.01
steps = int((t_f-t_0)/h)

#create plotting values
t = np.linspace(t_0,t_f,steps)
x = np.zeros(steps)
y = np.zeros(steps)
z = np.zeros(steps)

##### Relevant to the question again #####

init_condition_array = [[0,0,0],[0.1,0,0],[0.2,0,0],[0.3,0,0]]
color_array = ["red","orange","green","blue"]
color_counter = 0
zs_array = [0, 0.1, 0.2, 0.3]

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

for row in init_condition_array:
    x[0] = row[0]
    y[0] = row[1]
    z[0] = row[2]

    for i in range(x.size-1):
        #re-evaluate the values of the x-arrays depending on the initial conditions
        [x[i+1],y[i+1],z[i+1]]=rossler(x[i],y[i],z[i],t[i+1]-t[i],a,b,c)

    plt.plot(t,x,zs=zs_array[color_counter],zdir="z",color=color_array[color_counter])
    color_counter += 1

ax.set_xlabel('t')
ax.set_ylabel('x(t)')
plt.show()  

As you can see, the graphs should look incredibly different;

this is a 2D image of the graphs on the same axis with a few alterations to the code (shown below):

Whilst this is the graph produced by the 3D plot:

.

The 2D plot was created by making these small alterations to the code; nothing above the first line was changed:

init_condition_array = [[0,0,0],[0.1,0,0],[0.2,0,0],[0.3,0,0]]
color_array = ["red","orange","green","blue"]
color_counter = 0

fig = plt.figure()
ax = fig.add_subplot(111)

for row in init_condition_array:
    x[0] = row[0]
    y[0] = row[1]
    z[0] = row[2]

    for i in range(x.size-1):
        #re-evaluate the values of the x-arrays depending on the initial conditions
        [x[i+1],y[i+1],z[i+1]]=rossler(x[i],y[i],z[i],t[i+1]-t[i],a,b,c)

    plt.plot(t,x,color=color_array[color_counter],lw=1)
    color_counter += 1

ax.set_xlabel('t')
ax.set_ylabel('x(t)')
plt.show()  
Community
  • 1
  • 1
Ari Cooper-Davis
  • 3,374
  • 3
  • 26
  • 43
  • 3
    Moving `x = np.zeros(steps)` inside the `for row in init_condition_array` loop fixes/avoids the problem. It appears (somehow) `x` is stored inside the `Line3D` objects returned by `plt.plot`, but mutating `x` affects the values stored in the other `Line3D`s... – unutbu Apr 01 '16 at 13:33
  • 1
    Possible duplicate of [How can I tell if NumPy creates a view or a copy?](http://stackoverflow.com/questions/11524664/how-can-i-tell-if-numpy-creates-a-view-or-a-copy) – ivan_pozdeev Apr 01 '16 at 14:29

1 Answers1

1

Moving x = np.zeros(steps) inside the for row in init_condition_array loop fixes/avoids the problem. x is stored inside the Line3D objects returned by plt.plot, and mutating x affects the values stored in the other Line3Ds.

enter image description here


If you trace through the source code for Line3D you'll find that the data that you pass to plt.plot ends up in a Line3D's _verts3d attribute. The data is not copied; the _verts3d tuple holds references to the exact same arrays.

And this _verts3d attribute is directly accessed later when rendering:

def draw(self, renderer):
    xs3d, ys3d, zs3d = self._verts3d

Thus mutating the data -- even after calling plt.plot -- mutates self._verts3d. This simple example demonstrates the problem:

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

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
t = np.linspace(0, 1, 5)
x = np.sin(t)
line, = plt.plot(t, x, 0)

Here we have the original values of x:

print(line._verts3d[1])
# [ 0.          0.24740396  0.47942554  0.68163876  0.84147098]

And this shows that mutating x modifies line._verts3d:

x[:] = 1
print(line._verts3d[1])
# [ 1.  1.  1.  1.  1.]

# The result is a straight line, not a sine wave.
plt.show()

This surprising pitfall does not happen when making 2D line plots because there the Line2D._xy attribute which holds the data used for rendering stores a copy of the original data.


This problem could be fixed in the source code by changing this line in art3d.Line3D.set_3d_properties from

self._verts3d = art3d.juggle_axes(xs, ys, zs, zdir)

to

import copy
self._verts3d = copy.deepcopy(art3d.juggle_axes(xs, ys, zs, zdir))
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677