0

I am simulating sprinkler in 3D. In a previous question, I was helped to animate it in 2D (thank you @William Miller). I tried to use the same logic and implement it into 3D. In the update function, I get the following error:

'tuple' object is not callable
line 129 --> self.scat = self.ax.scatter(x,y,z, 'b.')

I looked at this answer when editing the code, which is why I used _offsets3d. I have looked through the Matplotlib documentation and tried to figure out why I'm getting a tuple object, but no success. I have also tried to simply use scatter on the given axes, but the kernel becomes unresponsive (even for 5 drops, 8 seconds, 50 frames). I'm not sure what to try next?

#Based on code by William Miller

import matplotlib
import matplotlib.pyplot as plt
from matplotlib import animation
import numpy as np
from math import sin, cos
from mpl_toolkits.mplot3d import Axes3D
import random

#Parameters
rho = 1.225
c = 0.5
v0 = 50
g = 9.81

#Timing
fps = 20
tmax = 1*10
nframes = tmax*fps
time = np.linspace(0,tmax, nframes)
dt = time[1]-time[0]

#Waterdroplets
ndrops = 100

#Positioning
maxs = [0.0, 0.0, 0.0]
rmax = [0.0008, 0.0065] #range of radii of water droplets in m
finx = []               #record landing positions of the droplets to be used for analysis
finy = []

#Droplet sizing
theta = np.radians(np.random.normal(37, 8, 80))
phi = np.radians(np.random.normal(3, 1.07, 80))
radii = np.random.normal(0.004, 0.001, 80)

class drop:

    def __init__(self,pos,vel,r):
        self.pos = pos
        self.vel = vel
        self.r = r

class sprinkler:

    def __init__(self):
        self.fig = plt.figure()
        self.ax = self.fig.add_subplot(111, projection = '3d')
        self.drops = [None]*ndrops    # creates empty list of length ndrops
        self.maxt = 0.0

        theta = np.radians(np.random.normal(37, 8, 100))
        phi = np.radians(np.random.normal(3, 1.07, 100))
        radii = np.random.normal(0.004, 0.001, 100)

        #Find the maximum flight time for each droplet 
        #and the maximum distance the droplets will travel

        for i in range(len(phi)):
            m = [drop([0.0, 0.0,0.1], [v0*cos(theta[i])*cos(phi[i]),
                                       v0*cos(theta[i])*sin(theta[i]),
                                       v0*sin(theta[i])],0.0008),
                 drop([0.0, 0.0,0.1], [v0*cos(theta[i])*cos(phi[i]),
                                       v0*cos(theta[i])*sin(theta[i]),
                                       v0*sin(theta[i])],0.0065)]
            for d in m:
                t = 0.0
                coef = -0.5*c*np.pi*d.r**2*rho
                mass = 4/3*np.pi*d.r**3*1000
                while d.pos[2] > 0:
                    a = np.power(d.vel, 2) * coef * np.sign(d.vel)/mass
                    a[2] -= g
                    d.pos += (d.vel + a * dt) * dt
                    d.vel += a * dt
                    t += dt
                    if d.pos[2] > maxs[2]:
                        maxs[2] = d.pos[2]                    
                    if d.pos[1] > maxs[1]:
                        maxs[1] = d.pos[1]
                    if d.pos[0] > maxs[0]:
                        maxs[0] = d.pos[0]
                    if d.pos[2] < 0.0:
                        if t > self.maxt:
                            self.maxt = t
                        break
        #print('Max time is:',maxt)
        #print('Max positions are:', maxs)


        #Create initial droplets
        for ii in range(ndrops):
            phiang = random.randint(0,len(phi)-1)
            thetang = random.randint(0,len(theta)-1)
            rad = random.randint(0,len(radii)-1)

            self.drops[ii] = drop([0.0, 0.0, 0.1],
                                 [v0*cos(theta[thetang])*cos(phi[phiang]),
                                  v0*cos(theta[thetang])*sin(phi[phiang]),
                                  v0*sin(theta[thetang])],
                                  radii[random.randint(0,len(radii)-1)])
        ani = animation.FuncAnimation(self.fig, self.update, init_func = self.setup,
                                          interval = 200, frames = nframes)
        ani.save('MySprinkler.mp4', fps = 20, extra_args=['-vcodec', 'libx264'])
        plt.show()

    def setup(self):
        self.scat = self.ax.scatter([d.pos[0] for d in self.drops],
                                    [d.pos[1] for d in self.drops],
                                    [d.pos[2] for d in self.drops], 'b.')

        self.ax.set_xlim(-1, 100)
        self.ax.set_ylim(-1, 100)
        self.ax.set_zlim(0, 50)
        self.ax.set_xlabel('X Distance')
        self.ax.set_ylabel('Y Distance')
        self.ax.set_zlabel('Height')

        return self.scat

    def update(self, frame):
        if time[frame] <(tmax-self.maxt*1.1):
            self.create(ndrops)
        self.step()
        for d in self.drops:
            x = d.pos[0]
            y = d.pos[1]
            z = d.pos[2]
            self.scat = self.scat._offsets3d(x,y,z, 'b.')

        return self.scat,

    def create(self, i):
        for l in range(i):
            phiang = random.randint(0,len(phi)-1)
            thetang = random.randint(0,len(theta)-1)
            rad = random.randint(0,len(radii)-1)
            self.drops.append(drop([0.0, 0.0, 0.0],
                                   [v0*cos(theta[thetang])*cos(phi[phiang]),
                                    v0*cos(theta[thetang])*sin(phi[phiang]),
                                    v0*sin(theta[thetang])],
                                   radii[rad]))

    def step(self):
        global finx, finy
        for x in range(len(self.drops)):
            coef = -0.5*c*np.pi*self.drops[x].r**2*rho
            mass = 4/3*np.pi*self.drops[x].r**3*1000
            a = np.power(self.drops[x].vel,2) * coef * np.sign(self.drops[x].vel)/mass
            a[2] = a[2]-g

            self.drops[x].pos += np.array(self.drops[x].vel)*dt +0.5*a*dt**2
            self.drops[x].vel += a*dt
            if self.drops[x].pos[2] < 0.0:
                self.drops[x].pos[2] = 0.0
                self.drops[x].vel = [0.0, 0.0, 0.0]
                finx = np.append(finx, self.drops[x].pos[0])
                finy = np.append(finy, self.drops[x].pos[1])
        return self.drops, finx, finy,

sprinkler()
Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
marlise23
  • 73
  • 1
  • 1
  • 6

1 Answers1

0

There seem to be 2 problems with the update function:

  • As the error message 'tuple' object is not callable indicates, _offsets3d(x,y,z, 'b.') is not a callable function. It is a tuple that holds x, y and z values.
  • These x,y,z can not be single values: they need to be in an array or list. So, you need to set all your drops at the same time.

I changed your update function to:

    def update(self, frame):
        if time[frame] <(tmax-self.maxt*1.1):
            self.create(ndrops)
        self.step()

        x = np.array([d.pos[0] for d in self.drops])
        y = np.array([d.pos[1] for d in self.drops])
        z = np.array([d.pos[2] for d in self.drops])
        self.scat._offsets3d = (x, y, z)

        return self.scat,

giving a working animation (I ried with 5 drops) as illustrated below.

I didn't investigate the speed issues. A small thing you could change, is directly saving the drops into the format needed by _offsets3d, but that isn't the real culprit. What probably helps more is to use numpy's broadcasting for the calculations. Also note that you're constantly adding drops to your list getting larger and larger.

demo plot

JohanC
  • 71,591
  • 8
  • 33
  • 66