Fields of a Moving Charge – manim Series: Part 11

The post is part of a series on learning how to use manim.  You can find the previous tutorial post in this series here and the overview of the entire series here.

Important Note:  These posts are based on an earlier version of manim which uses Python 2.7.  The latest version of manim is using Python 3.  To follow along with these posts, use Python 2.7 and the May 9, 2018 commit of manim .

11.0 Field of a Moving Charge

There was a question over on Reddit about how to create the electric field of a moving charge. Since that is something I will want to do at some point I figured it would be fun to give it a try.

Before creating a changing field, I thought I’d start with moving charges around. I know I saw this in one of the videos so I can start with working code and modify it to my needs. Here is what I came up with:

class MovingCharges(Scene):
CONFIG = {
"plane_kwargs" : {
"color" : RED_B
},
"point_charge_loc" : 0.5*RIGHT-1.5*UP,
}
def construct(self):
plane = NumberPlane(**self.plane_kwargs)
plane.main_lines.fade(.9)
plane.add(plane.get_axis_labels())
self.add(plane)

field = VGroup(*[self.calc_field(x*RIGHT+y*UP)
for x in np.arange(-9,9,1)
for y in np.arange(-5,5,1)
])
self.field=field
source_charge = self.Positron().move_to(self.point_charge_loc)
self.play(FadeIn(source_charge))
self.play(ShowCreation(field))
self.moving_charge()

def calc_field(self,point):
x,y = point[:2]
Rx,Ry = self.point_charge_loc[:2]
r = math.sqrt((x-Rx)**2 + (y-Ry)**2)
efield = (point - self.point_charge_loc)/r**3
return Vector(efield).shift(point)

def moving_charge(self):
numb_charges = 1
possible_points = [v.get_start() for v in self.field]
points = random.sample(possible_points, numb_charges)
particles = VGroup(*[
self.Positron().move_to(point)
for point in points
])
for particle in particles:
particle.velocity = np.array((0,0,0))

self.play(FadeIn(particles))
self.moving_particles = particles
self.add_foreground_mobjects(self.moving_particles )
self.always_continually_update = True
self.wait(10)

def field_at_point(self,point):
x,y = point[:2]
Rx,Ry = self.point_charge_loc[:2]
r = math.sqrt((x-Rx)**2 + (y-Ry)**2)
efield = (point - self.point_charge_loc)/r**3
return efield

def continual_update(self, *args, **kwargs):
if hasattr(self, "moving_particles"):
dt = self.frame_duration
for p in self.moving_particles:
accel = self.field_at_point(p.get_center())
p.velocity = p.velocity + accel*dt
p.shift(p.velocity*dt)

class Positron(Circle):
CONFIG = {
"radius" : 0.2,
"stroke_width" : 3,
"color" : RED,
"fill_color" : RED,
"fill_opacity" : 0.5,
}
def __init__(self, **kwargs):
Circle.__init__(self, **kwargs)
plus = TexMobject("+")
plus.scale(0.7)
plus.move_to(self)
self.add(plus)

The most important method here is continual_update(). This method updates the screen for each frame during the entire scene. This differs from the various transformations that rely on the play() method in that the transformations occur over a short time interval, usually on the order of a few seconds while the continual methods continue to run for the entire scene. If we want a particle to move across the screen we might be tempted to use something like self.play(ApplyMethod(particle1.shift,5*LEFT)) but it would be challenging to control the timing of other transformations going on at the same time. The continual_update() allows you to animate things in the background while still controlling the timing of other transformations.

Since I know I will be using charged particles in my videos I’ve written a Positron class to create positively charged particles. The positron is the positive antiparticle of the electron. Why didn’t I make it a proton? Because the proton is roughly 2000 times more massive and I want similarly sized particles for what I want to do.

We’ve reused the code from a previous post about electric fields but we’ve added methods to create the charged particles and move them around. moving_charge() is what creates positrons by randomly selecting a field point (possible_points = [v.get_start() for v in self.field] is a list of the locations of the tails of all field vectors) and then selects numb_charges points to create particles at. Note that the randomly generated charges don’t react to one another, which I find disturbing to watch because it isn’t physical.

particles is a vectorized mobject group that contains all of the moving charges with initial velocities set to zero (particle.velocity = np.array((0,0,0))). We could have simplified the code by only using one particle at a set location, but we’ll need multiple charges later on. The charges are then added to the screen (self.play(FadeIn(particles)) and assigned to a class variable that is needed in continual_update (self.moving_particles = particles). Mobjects are drawn in the order they are added to the screen but you can place certain mobjects in the foreground to insure they always remain drawn on top of other objects by using add_foreground_mobjects(). It is kind of like layers in Photoshop or similar software except each mobject is in its own layer. This has to do with the fact that manim keeps all mobjects drawn on the screen in a list and draws them in the order they are listed. There is no equivalent background mobject method, but you can send mobjects to the front or back layers with bring_to_front() and bring_to_back().

Next we tell manim to continually update things in the background (self.always_continually_update = True) and then wait ten seconds. It is important to set the wait() command because the continual update only runs as long as their are animation elements (play() commands) or wait() commands in the animation queue.

The field_at_point() method duplicates some of the earlier code but is used to return a numerical vector (a numpy 3-element array) rather than a mobject Vector, which is what calc_field() returns. It took me an embarassing amount of time to figure out why I couldn’t just use calc_field() to find the force vector.

The continual_update() method is called each frame when the scene is being composed. The first line, if hasattr(self, "moving_particles"): prevents the rest of the code running and throwing and error if you haven’t created self.moving_particles. The frame duration is either 1/15, 1/30, or 1/60 of a second, depending on whether your video is low, medium, or production quality (i.e. whether you include -l, -m, or no command line argument when extracting the scene). We run through the list of all moving particles (for p in self.moving_particles:) and then calculate the acceleration due to the electric field at the location of each particle (vect = self.field_at_point(p.get_center())). p.get_center() returns the vector location of each particle p. The velocity is updated using \vec{v}_f = \vec{v}_i + a \Delta t and then the particle is shifted over the distance \vec{v}_f \Delta t.

11.1 Updating the Electric Field of a Moving Charge

Now we’ve got some experience moving things around on the screen so we can move on to calculating the field due to the particle. We will reuse much of the code from our previous program, with a few changes.

class FieldOfMovingCharge(Scene):
CONFIG = {
"plane_kwargs" : {
"color" : RED_B
},
"point_charge_start_loc" : 5.5*LEFT-1.5*UP,
}
def construct(self):
plane = NumberPlane(**self.plane_kwargs)
plane.main_lines.fade(.9)
plane.add(plane.get_axis_labels())
self.add(plane)

field = VGroup(*[self.create_vect_field(self.point_charge_start_loc,x*RIGHT+y*UP)
for x in np.arange(-9,9,1)
for y in np.arange(-5,5,1)
])
self.field=field
self.source_charge = self.Positron().move_to(self.point_charge_start_loc)
self.source_charge.velocity = np.array((1,0,0))
self.play(FadeIn(self.source_charge))
self.play(ShowCreation(field))
self.moving_charge()

def create_vect_field(self,source_charge,observation_point):
return Vector(self.calc_field(source_charge,observation_point)).shift(observation_point)

def calc_field(self,source_point,observation_point):
x,y,z = observation_point
Rx,Ry,Rz = source_point
r = math.sqrt((x-Rx)**2 + (y-Ry)**2 + (z-Rz)**2)
if r<0.0000001: #Prevent divide by zero
efield = np.array((0,0,0))
else:
efield = (observation_point - source_point)/r**3
return efield

def moving_charge(self):
numb_charges=1
possible_points = [v.get_start() for v in self.field]
points = random.sample(possible_points, numb_charges)
particles = VGroup(self.source_charge, *[
self.Positron().move_to(point)
for point in points
])
for particle in particles[1:]:
particle.velocity = np.array((0,0,0))
self.play(FadeIn(particles[1:]))
self.moving_particles = particles
self.add_foreground_mobjects(self.moving_particles )
self.always_continually_update = True
self.wait(5)

def continual_update(self, *args, **kwargs):
Scene.continual_update(self, *args, **kwargs)
if hasattr(self, "moving_particles"):
dt = self.frame_duration

for v in self.field:
field_vect=np.zeros(3)
for p in self.moving_particles:
field_vect = field_vect + self.calc_field(p.get_center(), v.get_start())
v.put_start_and_end_on(v.get_start(), field_vect+v.get_start())

for p in self.moving_particles:
accel = np.zeros(3)
p.velocity = p.velocity + accel*dt
p.shift(p.velocity*dt)

class Positron(Circle):
CONFIG = {
"radius" : 0.2,
"stroke_width" : 3,
"color" : RED,
"fill_color" : RED,
"fill_opacity" : 0.5,
}
def __init__(self, **kwargs):
Circle.__init__(self, **kwargs)
plus = TexMobject("+")
plus.scale(0.7)
plus.move_to(self)
self.add(plus)

One change we’ve made is to let calc_field() return a numpy vector rather than a mobject Vector. This does mean adding in create_vect_field() to create the mobjects from the numpy vectors.

Since we want our source charge to be able to move we have to add that source charge to the particles list. Thus the Vgroup we create includes that source charge plus the randomly generated charges using particles = VGroup(self.source_charge, *[self.Positron().move_to(point) for point in points]). Remember that the asterisk in front of the list lets Python know that each element in the list should be broken out and treated as a separate argument for the VGroup() class. To help make sense of this line of code we can break it out into a less elegant form:

list_of_random_charges=[]
for point in points:
new_charge = self.Positron().move_toe(point)
list_of_random_charges.append(new_charge)
particles = VGroup(self.source_charge, list_of_random_charges[0], list_of_random_charges[1], list_of_random_charges[2])

Thus one line of code replaces several lines. The reason we use particles[1:] in the code defining the velocity and fading in the particles is that the source charge already has a velocity and is on screen so we don’t want to redefine the velocity or have it fade in again (which makes it blink).

In continual_update() we now need to calculate the field at each grid point at each time step. First we cycle through each field point (for v in self.field:). Since we want to add up the fields from several charges, we set the field vector to zero (field_vect=np.zeros(3)) and then add up the fields at that point due to each charge (field_vect = field_vect + self.calc_field(p.get_center(), v.get_start())). We need to redraw the field vectors by specifying the start and end points of the vector. The start point is the initial grid point where the vector starts (v.get_start()) and the tip of the arrow is a distance equal to the field vector plus the starting point (field_vect+v.get_start()).

I don’t have the particles react to the fields of the other particles. This looks very unrealistic to me but it should be easy enough to implement. I just wanted to put out a post that lays out the basics of how to get the field lines working.

As usual, the code can be found at https://github.com/zimmermant/manim_tutorial.

Next time I’ll look at how to work with SVG files in manim.

Advertisements
This entry was posted in Just for Fun, Programming and tagged , , , . Bookmark the permalink.

3 Responses to Fields of a Moving Charge – manim Series: Part 11

  1. Pingback: 3D Scenes – manim Series: Part 10 | Talking Physics

  2. Pingback: Learning How To Animate Videos Using manim Series – A Journey | Talking Physics

  3. Pingback: Working with SVG Files – manim Series: Part 12 | Talking Physics

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.