Skip to content

dipole module

Module contains the Dipole class.

The Dipole class represents a pair of oscillating point charges whose trajectories are updated at each time step during the simulation. The positive and negative charge pair are represented as _DipoleCharge objects, which are a subclass of the Charge base class. The trajectories of these charges are determined at each time step using the run method from the instantiated Simulation object, which accepts Charge and Dipole objects as initialization parameters.

All units are in SI.

Dipole

Oscillating dipole with a moment that is dependent on driving E field.

Class simulates an oscillating dipole with a time-dependent origin and has a dipole moment that is determined by the Lorentz oscillator equation of motion. The dipole moment is updated at each time step in the simulation by the Simulation object.

The Lorentz oscillator equation of motion is given by (Novotny Eq. 8.135):

\(d''(t) + \gamma_0*d'(t) + \omega_0^2*d(t) = E_d(t)*q^2/m_{eff}\)

where \(E_d(t)\) is the driving electric field (i.e. the component of the external electric field along the dipole's axis of polarization).

Parameters:

Name Type Description Default
omega_0 float

Natural angular frequency of dipole (units: rad/s).

required
origin Union[Tuple[float, float, float], Callable[[float], ndarray]]

List of x, y, and z values of dipole's origin (center of mass) or function with one input parameter for time that returns a 3 element list for x, y, and z values.

required
initial_r Tuple[float, float, float]

List of x, y, and z values for the initial displacement vector between the two point charges.

required
q float

Magnitude of the charge value of each point charge. Default is e.

required
m Union[float, Tuple[float, float]]

Mass of the two point charges or a 2 element list of the two masses if they are different. Default is m_e.

required

Exceptions:

Type Description
ValueError

Raised if the magnitude of the initial moment is zero.

Examples:

Below is an origin function that oscillates along the x-axis with an amplitude of 1e-10 m and angular frequency of 1e12*2*pi rad/s:

def fun_origin(t):
    return np.array((1e-10*np.cos(1e12*2*np.pi*t), 0, 0))

get_E_driving(self, field_type='Total')

Return the magnitude of the driving electric field.

The driving electric field is the component of the external electric field experienced by the charge along the direction of polarization. The returned field type (Total, Velocity, or Acceleration) can be specified.

Parameters:

Name Type Description Default
field_type str

Return either the Total, Velocity, or Acceleration field. Defaults to Total.

'Total'

Exceptions:

Type Description
ValueError

Input for field_type argument is invalid.

Returns:

Type Description
ndarray

Magnitude of the driving electric field at each time step.

Source code in pycharge/dipole.py
def get_E_driving(self, field_type: str = 'Total') -> ndarray:
    """Return the magnitude of the driving electric field.

    The driving electric field is the component of the external electric
    field experienced by the charge along the direction of polarization.
    The returned field type (`Total`, `Velocity`, or `Acceleration`) can
    be specified.

    Args:
        field_type: Return either the `Total`, `Velocity`, or
            `Acceleration` field. Defaults to `Total`.

    Raises:
        ValueError: Input for `field_type` argument is invalid.

    Returns:
        Magnitude of the driving electric field at each time step.
    """
    if field_type == 'Total':
        return np.linalg.norm(self.E_total, axis=0)
    if field_type == 'Velocity':
        return np.linalg.norm(self.E_vel, axis=0)
    if field_type == 'Acceleration':
        return np.linalg.norm(self.E_acc, axis=0)
    raise ValueError('Invalid field')

get_kinetic_energy(self, exclude_origin=True)

Return the kinetic energy of the dipole at each time step.

The kinetic energy of just the dipole moment can be determined by excluding the kinetic energy from the origin's movement.

Parameters:

Name Type Description Default
exclude_origin bool

Kinetic energy calculation excludes the movement of the dipole's origin. Defaults to True.

True

Returns:

Type Description
ndarray

Kinetic energy at each time step.

Source code in pycharge/dipole.py
def get_kinetic_energy(self, exclude_origin: bool = True) -> ndarray:
    """Return the kinetic energy of the dipole at each time step.

    The kinetic energy of just the dipole moment can be determined by
    excluding the kinetic energy from the origin's movement.

    Args:
        exclude_origin: Kinetic energy calculation excludes the movement of
            the dipole's origin. Defaults to `True`.

    Returns:
        Kinetic energy at each time step.
    """
    if exclude_origin:
        return 0.5*self.m_eff*np.linalg.norm(self.moment_vel, axis=0)**2
    charge_KE = (0.5*self.m_eff *
                 np.linalg.norm(self.moment_vel, axis=0)**2)
    return charge_KE  # Double KE since there are two charges

get_origin_position(self, magnitude=False)

Return the position of the dipole's origin at each time step.

Parameters:

Name Type Description Default
magnitude bool

Return the magnitude of the origin position instead of a 3D vector if True. Defaults to False.

False

Returns:

Type Description
ndarray

Origin at each time step, either the magnitude of the position (1D array of size N) or the position vector (2D array of size 3 x N).

Source code in pycharge/dipole.py
def get_origin_position(self, magnitude: bool = False) -> ndarray:
    """Return the position of the dipole's origin at each time step.

    Args:
        magnitude: Return the magnitude of the origin position instead of a
            3D vector if `True`. Defaults to `False`.

    Returns:
        Origin at each time step, either the magnitude of the position (1D
        array of size N) or the position vector (2D array of size 3 x N).

    """
    origin_position = np.zeros((3, self.t_index+1))
    for i in np.arange(self.t_index+1):
        origin_position[:, i] = self.origin(self.dt*i)
    if magnitude:
        return np.linalg.norm(origin_position, axis=0)
    return origin_position

reset(self, timesteps, dt, save_E)

Initialize the moment and E arrays, and _DipoleCharge pair.

Source code in pycharge/dipole.py
def reset(self, timesteps: float, dt: float, save_E: bool) -> None:
    """Initialize the moment and E arrays, and `_DipoleCharge` pair."""
    self.t_index = 0
    self.dt = dt
    self.moment_disp = np.ones((3, timesteps))*np.inf
    self.moment_vel = np.ones((3, timesteps))*np.inf
    self.moment_acc = np.ones((3, timesteps))*np.inf
    self.moment_disp[:, 0] = self.initial_r
    self.moment_vel[:, 0] = 0
    self.moment_acc[:, 0] = 0
    if save_E:
        self.E_total = np.ones((3, timesteps))*np.inf
        self.E_vel = np.ones((3, timesteps))*np.inf
        self.E_acc = np.ones((3, timesteps))*np.inf
    for charge in self.charge_pair:
        charge.t_index = 0
        charge.dt = dt
        charge.position = np.ones((3, timesteps))*np.inf
        charge.velocity = np.ones((3, timesteps))*np.inf
        charge.acceleration = np.ones((3, timesteps))*np.inf
        m_frac = self.m[1]/(self.m[0]+self.m[1])  # Determine COM
        if charge.positive_charge:  # Set initial position
            charge.position[:, 0] = self.origin(0) + self.initial_r*m_frac
        else:
            charge.position[:, 0] = (self.origin(0)
                                     - self.initial_r*(1-m_frac))
        charge.velocity[:, 0] = 0
        charge.acceleration[:, 0] = 0

update_timestep(self, moment_disp, moment_vel, moment_acc, E_driving)

Update the array attributes at each time step.

Source code in pycharge/dipole.py
def update_timestep(
    self,
    moment_disp: ndarray,
    moment_vel: ndarray,
    moment_acc: ndarray,
    E_driving: Union[None, Tuple[ndarray, ndarray, ndarray]]
) -> None:
    """Update the array attributes at each time step."""
    self.t_index += 1
    self.moment_disp[:, self.t_index] = moment_disp
    self.moment_vel[:, self.t_index] = moment_vel
    self.moment_acc[:, self.t_index] = moment_acc
    if E_driving is not None:
        self.E_total[:, self.t_index] = E_driving[0]
        self.E_vel[:, self.t_index] = E_driving[1]
        self.E_acc[:, self.t_index] = E_driving[2]
    t = self.dt*self.t_index
    h = 1e-21  # Limit used to calcuate derivatives of the origin position
    origin_vel = (self.origin(t+h)-self.origin(t-h))/(2*h)
    origin_acc = (self.origin(t+h)-2*self.origin(t)+self.origin(t-h))/h**2
    m_frac = self.m[1]/(self.m[0]+self.m[1])  # Determine COM
    self.charge_pair[0].update_timestep(  # Update first charge
        self.origin(t)+moment_disp*m_frac,
        origin_vel+moment_vel*m_frac, origin_acc+moment_acc*m_frac
    )
    self.charge_pair[1].update_timestep(  # Update second charge
        self.origin(t)-moment_disp*(1-m_frac),
        origin_vel-moment_vel*(1-m_frac), origin_acc-moment_acc*(1-m_frac)
    )