simulation module
Module contains the Simulation
class and helper functions for save files.
The Simulation
class is used to calculate the electromagnetic fields and
potentials generated by moving point charges, which are defined by the Charge
base class. The values are determined numerically at each grid point by
evaluating the Liénard–Wiechert potentials and corresponding electromagnetic
field equations at the retarded time.
The Simulation
class also runs simulations using the Dipole
class by
dynamically calculating the dipole moment at each time step using the
run
method. Treating the dipole as a Lorentz oscillator, the differential
equation of motion is solved at each time step using the RK4 method. The
driving electric field acting on the dipole is the component of the external
electric field in the direction of polarization generated by the other point
charges in the simulation. After the simulation is run, the Simulation
object
can calculate the electromagnetic fields and potentials generated by the
Dipole
objects within the timespan of the simulation.
All units are in SI.
Simulation
Primary class for electromagnetism calculations using PyCharge
.
The Simulation
class calculates the electromagnetic fields and potentials
generated by point charges that are subclasses of the Charge
base class
and have predefined trajectories.
The Simulation
class also run simulations with Dipole
objects over a
range of time steps to dynamically determine their dipole moment. The
dipoles behave as Lorentz oscillators and their dipole moment and related
trajectories are calculated at each time step.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
sources |
Union[Sequence, Union[Charge, Dipole]] |
Either a single or
list of |
required |
tol |
float |
Tolerance for the scipy.optimize.newton method.
Default is |
required |
calculate_A(self, t, x, y, z, exclude_charges=None)
Calculate the vector potential (A) generated by the point charge(s).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
t |
float |
Time of simulation. |
required |
x |
ndarray |
Meshgrid of x values. |
required |
y |
ndarray |
Meshgrid of y values. |
required |
z |
ndarray |
Meshgrid of z values. |
required |
exclude_charges |
Sequence |
List of charges to exclude in calculation of the potentials. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
Tuple[ndarray, ndarray, ndarray] |
List of Ax, Ay, and Az. |
Source code in pycharge/simulation.py
def calculate_A(
self,
t: float,
x: ndarray,
y: ndarray,
z: ndarray,
exclude_charges: Sequence = None
) -> Tuple[ndarray, ndarray, ndarray]:
"""Calculate the vector potential (A) generated by the point charge(s).
Args:
t: Time of simulation.
x: Meshgrid of x values.
y: Meshgrid of y values.
z: Meshgrid of z values.
exclude_charges: List of charges to exclude in calculation of the
potentials. Defaults to None.
Returns:
List of Ax, Ay, and Az.
"""
t_array = np.ones((x.shape))
t_array[:, :, :] = t
Ax = np.zeros((x.shape))
Ay = np.zeros((x.shape))
Az = np.zeros((x.shape))
for charge in self.all_charges:
if exclude_charges is not None and charge in exclude_charges:
continue
initial_guess = -1e-12*np.ones((x.shape))
tr = optimize.newton(charge.solve_time, initial_guess,
args=(t_array, x, y, z), tol=self.tol)
rx = x - charge.xpos(tr)
ry = y - charge.ypos(tr)
rz = z - charge.zpos(tr)
r_mag = (rx**2 + ry**2 + rz**2)**0.5
vx = charge.xvel(tr) # retarded velocity - Griffiths Eq. 10.54
vy = charge.yvel(tr)
vz = charge.zvel(tr)
r_dot_v = rx*vx + ry*vy + rz*vz
# Griffiths Eq. 10.53
individual_V = charge.q*c/(4*pi*eps*(r_mag*c-r_dot_v))
# Griffiths Eq. 10.53
Ax += vx/c**2*individual_V
Ay += vy/c**2*individual_V
Az += vz/c**2*individual_V
return (Ax, Ay, Az)
calculate_B(self, t, x, y, z, pcharge_field='Total', exclude_charges=None)
Calculate the magnetic field (B) generated by the point charge(s).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
t |
float |
Time of simulation. |
required |
x |
ndarray |
Meshgrid of x values. |
required |
y |
ndarray |
Meshgrid of y values. |
required |
z |
ndarray |
Meshgrid of z values. |
required |
pcharge_field |
str |
Determines which field is returned:
|
'Total' |
exclude_charges |
Sequence |
List of charges to exclude in calculation of the magnetic field. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
ndarray |
List of Bx, By, and Bz. |
Source code in pycharge/simulation.py
def calculate_B(
self,
t: float,
x: ndarray,
y: ndarray,
z: ndarray,
pcharge_field: str = 'Total',
exclude_charges: Sequence = None
) -> ndarray:
"""Calculate the magnetic field (B) generated by the point charge(s).
Args:
t: Time of simulation.
x: Meshgrid of x values.
y: Meshgrid of y values.
z: Meshgrid of z values.
pcharge_field: Determines which field is returned:
`Velocity`, `Acceleration`, or `Total`. Defaults to `Total`.
exclude_charges: List of charges to exclude in calculation of the
magnetic field. Defaults to None.
Returns:
List of Bx, By, and Bz.
"""
t_array = np.ones((x.shape))
t_array[:, :, :] = t
Bx = np.zeros((x.shape))
By = np.zeros((x.shape))
Bz = np.zeros((x.shape))
for charge in self.all_charges:
if exclude_charges is not None and charge in exclude_charges:
continue
initial_guess = -1e-12*np.ones((x.shape))
tr = optimize.newton(charge.solve_time, initial_guess,
args=(t_array, x, y, z), tol=self.tol)
E_x, E_y, E_z = self._calculate_individual_E(
charge, tr, x, y, z, pcharge_field)
rx = x - charge.xpos(tr)
ry = y - charge.ypos(tr)
rz = z - charge.zpos(tr)
r_mag = (rx**2 + ry**2 + rz**2)**0.5
Bx += 1/(c*r_mag)*(ry*E_z-rz*E_y)
By += 1/(c*r_mag)*(rz*E_x-rx*E_z)
Bz += 1/(c*r_mag)*(rx*E_y-ry*E_x)
return np.array((Bx, By, Bz))
calculate_E(self, t, x, y, z, pcharge_field='Total', exclude_charges=None)
Calculate the electric field (E) generated by the point charge(s).
Meshgrid must use matrix indexing ('ij').
Parameters:
Name | Type | Description | Default |
---|---|---|---|
t |
float |
Time of simulation. |
required |
x |
ndarray |
Meshgrid of x values. |
required |
y |
ndarray |
Meshgrid of y values. |
required |
z |
ndarray |
Meshgrid of z values. |
required |
pcharge_field |
str |
Determines which field is returned: |
'Total' |
exclude_charges |
Sequence |
List of charges to exclude in calculation of the electric field. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
ndarray |
List of E_x, E_y, and E_z. |
Source code in pycharge/simulation.py
def calculate_E(
self,
t: float,
x: ndarray,
y: ndarray,
z: ndarray,
pcharge_field: str = 'Total',
exclude_charges: Sequence = None
) -> ndarray:
"""Calculate the electric field (E) generated by the point charge(s).
Meshgrid must use matrix indexing ('ij').
Args:
t: Time of simulation.
x: Meshgrid of x values.
y: Meshgrid of y values.
z: Meshgrid of z values.
pcharge_field: Determines which field is returned: `Velocity`,
`Acceleration`, or `Total`. Defaults to `Total`.
exclude_charges: List of charges to exclude in calculation of the
electric field. Defaults to None.
Returns:
List of E_x, E_y, and E_z.
"""
t_array = np.ones((x.shape))
t_array[:, :, :] = t
E_x = np.zeros((x.shape))
E_y = np.zeros((x.shape))
E_z = np.zeros((x.shape))
for charge in self.all_charges:
if exclude_charges is not None and charge in exclude_charges:
continue
initial_guess = -1e-12*np.ones((x.shape))
tr = optimize.newton(charge.solve_time, initial_guess,
args=(t_array, x, y, z), tol=self.tol)
E_field = self._calculate_individual_E(
charge, tr, x, y, z, pcharge_field)
E_x += E_field[0]
E_y += E_field[1]
E_z += E_field[2]
return np.array((E_x, E_y, E_z))
calculate_V(self, t, x, y, z, exclude_charges=None)
Calculate the scalar potential (V) generated by the point charge(s).
Parameters:
Name | Type | Description | Default |
---|---|---|---|
t |
float |
Time of simulation. |
required |
x |
ndarray |
Meshgrid of x values. |
required |
y |
ndarray |
Meshgrid of y values. |
required |
z |
ndarray |
Meshgrid of z values. |
required |
exclude_charges |
Sequence |
List of charges to exclude in calculation of the potentials. Defaults to None. |
None |
Returns:
Type | Description |
---|---|
ndarray |
Scalar potential V. |
Source code in pycharge/simulation.py
def calculate_V(
self,
t: float,
x: ndarray,
y: ndarray,
z: ndarray,
exclude_charges: Sequence = None
) -> ndarray:
"""Calculate the scalar potential (V) generated by the point charge(s).
Args:
t: Time of simulation.
x: Meshgrid of x values.
y: Meshgrid of y values.
z: Meshgrid of z values.
exclude_charges: List of charges to exclude in calculation of the
potentials. Defaults to None.
Returns:
Scalar potential V.
"""
t_array = np.ones((x.shape))
t_array[:, :, :] = t
V = np.zeros((x.shape))
for charge in self.all_charges:
if exclude_charges is not None and charge in exclude_charges:
continue
initial_guess = -1e-12*np.ones((x.shape))
tr = optimize.newton(charge.solve_time, initial_guess,
args=(t_array, x, y, z), tol=self.tol)
rx = x - charge.xpos(tr)
ry = y - charge.ypos(tr)
rz = z - charge.zpos(tr)
r_mag = (rx**2 + ry**2 + rz**2)**0.5
vx = charge.xvel(tr) # retarded velocity - Griffiths Eq. 10.54
vy = charge.yvel(tr)
vz = charge.zvel(tr)
r_dot_v = rx*vx + ry*vy + rz*vz
# Griffiths Eq. 10.53
V += charge.q*c/(4*pi*eps*(r_mag*c-r_dot_v))
return V
run(self, timesteps, dt, file=None, save_E=False, max_vel=2997924.58, progressbar=True)
Run simulation with Dipole
and Charge
object(s).
Simulation calculates the dipole moment and corresponding derivatives
at each time step by solving the equation of motion using the RK4
method. The position, velocity, and acceleration values are then
updated in the Dipole
object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
timesteps |
int |
Number of time steps run during the simulation. |
required |
dt |
float |
Time step. |
required |
file |
Optional[str] |
File name to load/save |
None |
save_E |
bool |
If True, the driving field acting on each dipole is
saved at each time step. However, this comes at a cost to speed
and memory. Defaults to |
False |
max_vel |
float |
The maximum possible velocity achieved by the two point
charges in |
2997924.58 |
progressbar |
bool |
Prints the progressbar if |
True |
Exceptions:
Type | Description |
---|---|
ValueError |
Raised if the point charge's velocity in the |
Source code in pycharge/simulation.py
def run(
self,
timesteps: int,
dt: float,
file: Optional[str] = None,
save_E: bool = False,
max_vel: float = c/100,
progressbar: bool = True
) -> None:
"""Run simulation with `Dipole` and `Charge` object(s).
Simulation calculates the dipole moment and corresponding derivatives
at each time step by solving the equation of motion using the RK4
method. The position, velocity, and acceleration values are then
updated in the `Dipole` object.
Args:
timesteps (int): Number of time steps run during the simulation.
dt (float): Time step.
file: File name to load/save `Simulation`. Defaults to `None`.
save_E (bool): If True, the driving field acting on each dipole is
saved at each time step. However, this comes at a cost to speed
and memory. Defaults to `True`.
max_vel: The maximum possible velocity achieved by the two point
charges in `Dipole` object. Defaults to `c/100`.
progressbar: Prints the progressbar if `True`. Defaults to `True`.
Raises:
ValueError: Raised if the point charge's velocity in the `Dipole`
object becomes larger than `max_vel`.
"""
self._init_simulation(timesteps, dt, save_E)
if file is not None and self._load(file):
return
for tstep in tqdm(range(self.timesteps-1), total=self.timesteps,
initial=1, mininterval=0.5, disable=not progressbar):
for dipole in self.dipoles:
moment_disp, moment_vel = self._rk4(tstep, dipole)
self._update_dipole(tstep, dipole, moment_disp, moment_vel,
max_vel)
if file is not None:
self._save(file)
run_mpi(self, timesteps, dt, file=None, save_E=False, max_vel=2997924.58, progressbar=True)
Run simulation with Dipole
and Charge
object(s) using MPI.
Method exploits MPI to speed up the simulation. In the ideal case, the
number of MPI processors and the number of Dipole
objects are equal.
Most efficient simulation occurs when save_E
is False
.
Simulation calculates the dipole moment and corresponding derivatives
at each time step by solving the equation of motion using the RK4
method. The position, velocity, and acceleration values are then
updated in the Dipole
object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
timesteps |
int |
Number of time steps run during the simulation. |
required |
dt |
float |
Time step. |
required |
file |
Optional[str] |
File name to load/save |
None |
save_E |
bool |
If True, the driving field acting on each dipole is
saved at each time step. However, this comes at a cost to speed
and memory. Defaults to |
False |
max_vel |
float |
The maximum possible velocity achieved by the two point
charges in |
2997924.58 |
progressbar |
bool |
Prints the progressbar if |
True |
Exceptions:
Type | Description |
---|---|
NotImplementedError |
Raised if |
ValueError |
Raised if the point charge's velocity in the |
Examples:
An MPI program can be run from a script with the command mpiexec: >> $ mpiexec -n 4 python script.py Here, 4 separate processes will be initiated to run script.py.
Source code in pycharge/simulation.py
def run_mpi(
self,
timesteps: int,
dt: float,
file: Optional[str] = None,
save_E: bool = False,
max_vel: float = c/100,
progressbar: bool = True
) -> None:
"""Run simulation with `Dipole` and `Charge` object(s) using MPI.
Method exploits MPI to speed up the simulation. In the ideal case, the
number of MPI processors and the number of `Dipole` objects are equal.
Most efficient simulation occurs when `save_E` is `False`.
Simulation calculates the dipole moment and corresponding derivatives
at each time step by solving the equation of motion using the RK4
method. The position, velocity, and acceleration values are then
updated in the `Dipole` object.
Args:
timesteps (int): Number of time steps run during the simulation.
dt (float): Time step.
file: File name to load/save `Simulation`. Defaults to `None`.
save_E (bool): If True, the driving field acting on each dipole is
saved at each time step. However, this comes at a cost to speed
and memory. Defaults to `True`.
max_vel: The maximum possible velocity achieved by the two point
charges in `Dipole` object. Defaults to `c/100`.
progressbar: Prints the progressbar if `True`. Defaults to `True`.
Raises:
NotImplementedError: Raised if `mpi4py` package is not installed.
ValueError: Raised if the point charge's velocity in the `Dipole`
object becomes larger than `max_vel`.
Example:
An MPI program can be run from a script with the command mpiexec:
>> $ mpiexec -n 4 python script.py
Here, 4 separate processes will be initiated to run script.py.
"""
try:
from mpi4py import MPI # pylint: disable=import-outside-toplevel
except ImportError:
NotImplementedError('mpi4py package is not installed.')
self._init_simulation(timesteps, dt, save_E)
if file is not None and self._load(file):
return
mpi_comm = MPI.COMM_WORLD
mpi_size = mpi_comm.Get_size() # Number of MPI processes
mpi_rank = mpi_comm.Get_rank() # Rank of current MPI process
process_dipoles = self.dipoles[mpi_rank::mpi_size] # Dipoles evaluated
disable_bar = not progressbar or mpi_rank != 0
for tstep in tqdm(range(self.timesteps-1), total=self.timesteps,
initial=1, mininterval=0.5, disable=disable_bar):
moment_data = []
for dipole in process_dipoles: # Only evaluate `process_dipoles`
moment_disp, moment_vel = self._rk4(tstep, dipole)
self._update_dipole(tstep, dipole, moment_disp, moment_vel,
max_vel)
moment_data.append((moment_disp, moment_vel))
# Broadcast the dipole moment disp. and vel. to other MPI processes
for bcast_rank in range(mpi_size): # Send moment data
if mpi_rank == bcast_rank:
mpi_comm.bcast(moment_data, root=bcast_rank)
else: # Receive moment data and update dipoles
bcast_data = None
bcast_data = mpi_comm.bcast(bcast_data, root=bcast_rank)
for dipole, data in zip(self.dipoles[bcast_rank::mpi_size],
bcast_data):
self._update_dipole(tstep, dipole, data[0], data[1],
max_vel)
if mpi_rank == 0 and file is not None:
self._save(file)
combine_files(input_files, new_file)
Combine Simulation
objects from many .dat
files into a single file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
input_files |
Tuple[str, ...] |
List of names of files to be concatenated. |
required |
new_file |
str |
Name of output file, should end in |
required |
Source code in pycharge/simulation.py
def combine_files(input_files: Tuple[str, ...], new_file: str) -> None:
"""Combine `Simulation` objects from many `.dat` files into a single file.
Args:
input_files: List of names of files to be concatenated.
new_file: Name of output file, should end in `.dat`.
"""
for file in input_files:
with open(file, "rb") as f:
loaded_simulation = pickle.load(f)
with open(new_file, "ab") as new_f:
pickle.dump(loaded_simulation, new_f)
get_file_length(file)
Return the number of Simulation
objects in .dat
file.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
file |
str |
File name. |
required |
Returns:
Type | Description |
---|---|
int |
Number of |
Source code in pycharge/simulation.py
def get_file_length(file: str) -> int:
"""Return the number of `Simulation` objects in `.dat` file.
Args:
file: File name.
Returns:
Number of `Simulation` objects in file, `-1` if file not found.
"""
length = 0
try:
with open(file, "rb") as f:
while True:
pickle.load(f)
length += 1
except EOFError:
return length
except FileNotFoundError:
return -1