Source code for sarkas.time_evolution.integrators

"""
Module of various types of time_evolution
"""

from copy import deepcopy
from numba import float64, int64, jit, void
from numpy import arange, array, cos, cross, pi, sin, sqrt, zeros
from scipy.linalg import norm


[docs]class Integrator: """ Class used to assign integrator type. Attributes ---------- dt : float Timestep. kB : float Boltzmann constant. magnetized : bool Magnetized simulation flag. species_num : numpy.ndarray Number of particles of each species. species_plasma_frequencies : numpy.ndarray Plasma frequency of each species. box_lengths : numpy.ndarray Length of each box side. pbox_lengths : numpy.ndarray Initial particle box sides' lengths. verbose : bool Verbose output flag. type : str Integrator type. """ dt: float = None kB: float = None # attributes type: str = None supported_integrators = {} equilibration_type: str = "verlet" magnetization_type: str = "magnetic_verlet" production_type: str = "verlet" species_num = None species_plasma_frequencies = None # Thermostat attributes thermalization: bool = True thermostat_type: str = "berendsen" thermalization_rate: float = 2.0 thermalization_timestep: int = 0 berendsen_tau: float = None thermostat_temperatures = None thermostat_temperatures_eV = None # Magnetic attributes magnetized: bool = False magnetic_field_uvector = None magnetic_field = None omega_c = None species_cyclotron_frequencies = None ccodt = None cdt = None ssodt = None sdt = None v_B = None v_F = None # Langevin attributes c1 = None c2 = None sigma = None box_lengths = None pbox_lengths = None boundary_conditions = None supported_boundary_conditions = {} verbose: bool = False # def __repr__(self): # sortedDict = dict(sorted(self.__dict__.items(), key=lambda x: x[0].lower())) # disp = 'Integrator( \n' # for key, value in sortedDict.items(): # disp += "\t{} : {}\n".format(key, value) # disp += ')' # return disp def __copy__(self): """Make a shallow copy of the object using copy by creating a new instance of the object and copying its __dict__.""" # Create a new object _copy = type(self)() # copy the dictionary _copy.from_dict(input_dict=self.__dict__) return _copy def __deepcopy__(self, memodict={}): """Make a deepcopy of the object. Parameters ---------- memodict: dict Dictionary of id's to copies Returns ------- _copy: :class:`sarkas.time_evolution.integrators.Integrator` A new Integrator class. """ id_self = id(self) # memorization avoids unnecessary recursion _copy = memodict.get(id_self) if _copy is None: _copy = type(self)() # Make a deepcopy of the mutable arrays using numpy copy function for k, v in self.__dict__.items(): if k != "thread_ls": _copy.__dict__[k] = deepcopy(v, memodict) return _copy
[docs] def from_dict(self, input_dict: dict): """ Update attributes from input dictionary. Parameters ---------- input_dict: dict Dictionary to be copied. """ self.__dict__.update(input_dict)
[docs] def copy_params(self, params): """ Copy necessary parameters. Parameters ---------- params: :class:`sarkas.core.Parameters` Simulation's parameters. """ self.box_lengths = params.box_lengths self.pbox_lengths = params.pbox_lengths self.dimensions = params.dimensions self.kB = params.kB self.eV2K = params.eV2K self.total_num_ptcls = params.total_num_ptcls self.species_num = params.species_num.copy() self.species_plasma_frequencies = params.species_plasma_frequencies.copy() self.species_masses = params.species_masses.copy() self.species_temperatures = params.species_temperatures.copy() self.verbose = params.verbose # Enforce consistency if not self.boundary_conditions: self.boundary_conditions = params.boundary_conditions.lower() # Check whether you input temperatures in eV or K if self.thermalization and self.thermostat_temperatures: self.thermostat_temperatures_eV = self.thermostat_temperatures.copy() / self.eV2K elif self.thermalization and self.thermostat_temperatures_eV: self.thermostat_temperatures = self.thermostat_temperatures_eV.copy() * self.eV2K elif self.thermalization and not self.thermostat_temperatures: self.thermostate_temperatures = params.species_temperatures.copy() self.thermostate_temperatures_eV = params.species_temperatures_eV.copy() # Backwards compatibility if hasattr(self, "equilibration_steps"): params.equilibration_steps = self.equilibration_steps if hasattr(self, "magnetization_steps"): params.equilibration_steps = self.magnetization_steps if hasattr(self, "production_steps"): params.production_steps = self.production_steps if hasattr(self, "eq_dump_step"): params.eq_dump_step = self.eq_dump_step if hasattr(self, "mag_dump_step"): params.mag_dump_step = self.mag_dump_step if hasattr(self, "eq_dump_step"): params.prod_dump_step = self.prod_dump_step if not self.boundary_conditions: self.boundary_conditions = params.boundary_conditions if not hasattr(params, "boundary_conditions"): params.boundary_conditions = self.boundary_conditions if params.magnetized: self.magnetized = True self.magnetic_field = params.magnetic_field.copy() self.species_cyclotron_frequencies = params.species_cyclotron_frequencies.copy()
[docs] def setup(self, params, potential): """ Assign attributes from simulation's parameters and classes. Parameters ---------- params : :class:`sarkas.core.Parameters` Parameters class. potential : :class:`sarkas.potentials.core.Potential` Potential class. """ if self.dt is None: raise ValueError("integrator.dt is None. Please define Integrator.dt") self.copy_params(params) if self.magnetized: self.magnetic_setup() if self.type: self.type = self.type.lower() self.equilibration_type = self.type self.production_type = self.type self.boundary_condition_setup() if self.thermalization: self.thermostat_setup() self.pot_acc_setup(potential)
[docs] def pot_acc_setup(self, potential): """ Link the :meth:`.update_accelerations` method depending on the potential algorithm. Parameters ---------- potential : :class:`sarkas.potentials.core.Potential` Potential class. """ self.potential_type = potential.type if potential.method != "fmm": if potential.pppm_on: self.update_accelerations = potential.update_pppm else: if potential.linked_list_on: self.update_accelerations = potential.update_linked_list else: self.update_accelerations = potential.update_brute else: self.update_accelerations = ( potential.update_fmm_coulomb if potential.type == "coulomb" else potential.update_fmm_yukawa )
[docs] def boundary_condition_setup(self): self.supported_boundary_conditions = { "periodic": self.periodic_bc, "absorbing": self.absorbing_bc, "reflective": self.reflecting_bc, "open": self.open_bc, } msg = ( f"Unsupported boundary conditions. " f"Choose one of the supported boundary conditions\n{self.supported_boundary_conditions.keys()}", ) # Assign integrator.enforce_bc to the correct method self.enforce_bc = self.supported_boundary_conditions.get(self.boundary_conditions, ValueError(msg))
[docs] def thermostat_setup(self): """ Assign attributes from simulation's parameters. Raises ------ ValueError If a thermostat different than Berendsen is chosen. """ self.thermostat_type = self.thermostat_type.lower() if self.thermostat_type != "berendsen": raise ValueError("Only Berendsen thermostat is supported.") if self.berendsen_tau: self.thermalization_rate = 1.0 / self.berendsen_tau else: self.berendsen_tau = 1.0 / self.thermalization_rate
[docs] def type_setup(self, int_type): """ Parameters ---------- int_type: str Integrator type to use. Raises ------ : ValueError If `int_type` is not a supported integrator. """ # if int_type not in self.supported_integrators: # raise ValueError( # "Integrator not supported. " "Please choose one of the supported integrators \n", # self.supported_integrators, # ) # Assign integrator.update to the correct method if int_type == "langevin": self.sigma = sqrt(2.0 * self.langevin_gamma * self.kB * self.species_temperatures / self.species_masses) self.c1 = 1.0 - 0.5 * self.langevin_gamma * self.dt self.c2 = 1.0 / (1.0 + 0.5 * self.langevin_gamma * self.dt) elif int_type == "magnetic_verlet": # Calculate functions for magnetic integrator # This could be used when the generalization to Forest-Ruth and MacLachlan algorithms will be implemented # In a magnetic Velocity-Verlet the coefficient is 1/2, see eq.~(78) in :cite:`Chin2008` self.magnetic_helpers(0.5) if self.magnetic_field_uvector @ array([0.0, 0.0, 1.0]) == 1.0: # dot product int_type = "magnetic_verlet_zdir" elif int_type == "magnetic_pos_verlet": # Calculate functions for magnetic integrator # This could be used when the generalization to Forest-Ruth and MacLachlan algorithms will be implemented # In a magnetic Velocity-Verlet the coefficient is 1/2, see eq.~(78) in :cite:`Chin2008` self.magnetic_helpers(1.0) if self.magnetic_field_uvector @ array([0.0, 0.0, 1.0]) == 1.0: # dot product int_type = "magnetic_pos_verlet_zdir" elif int_type == "magnetic_boris": # In a leapfrog-type algorithm the coefficient is different for the acceleration and magnetic rotation # see eq.~(79) in :cite:`Chin2008` self.magnetic_helpers(1.0) if self.magnetic_field_uvector @ array([0.0, 0.0, 1.0]) == 1.0: # dot product int_type = "magnetic_boris_zdir" elif int_type == "cyclotronic": # Calculate functions for magnetic integrator # This could be used when the generalization to Forest-Ruth and MacLachlan algorithms will be implemented # In a magnetic Velocity-Verlet the coefficient is 1/2, see eq.~(78) in :cite:`Chin2008` self.magnetic_helpers(0.5) if self.magnetic_field_uvector @ array([0.0, 0.0, 1.0]) == 1.0: # dot product int_type = "cyclotronic_zdir" self.supported_integrators = { "verlet": self.verlet, "langevin": self.langevin, "magnetic_verlet": self.magnetic_verlet, "magnetic_verlet_zdir": self.magnetic_verlet_zdir, "magnetic_pos_verlet": self.magnetic_pos_verlet, "magnetic_pos_verlet_zdir": self.magnetic_pos_verlet_zdir, "magnetic_boris": self.magnetic_boris, "magnetic_boris_zdir": self.magnetic_boris_zdir, "cyclotronic": self.cyclotronic, "cyclotronic_zdir": self.cyclotronic_zdir, } msg = f"Integrator not supported. Please choose one of the supported integrators \n{self.supported_integrators.keys()}" return self.supported_integrators.get(int_type, ValueError(msg))
[docs] def magnetic_setup(self): # Create the unit vector of the magnetic field self.magnetic_field_uvector = self.magnetic_field / norm(self.magnetic_field) self.omega_c = zeros((self.total_num_ptcls, 3)) sp_start = 0 sp_end = 0 for ic, sp_np in enumerate(self.species_num): sp_end += sp_np self.omega_c[sp_start:sp_end, :] = self.species_cyclotron_frequencies[ic] sp_start += sp_np # array to temporary store velocities # Luciano: I have the vague doubt that allocating memory for these arrays is faster than calculating them # each time step self.v_B = zeros((self.total_num_ptcls, 3)) self.v_F = zeros((self.total_num_ptcls, 3))
[docs] def langevin(self, ptcls): """ Update particles class using the velocity verlet algorithm and Langevin damping. Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. """ beta = ptcls.gaussian(0.0, 1.0, (self.total_num_ptcls, self.dimensions)) sp_start = 0 # start index for species loop sp_end = 0 for ic, num in enumerate(self.species_num): sp_end += num ptcls.pos[sp_start:sp_end, : self.dimensions] += ( self.c1 * self.dt * ptcls.vel[sp_start:sp_end, : self.dimensions] + 0.5 * self.dt**2 * ptcls.acc[sp_start:sp_end, : self.dimensions] + 0.5 * self.sigma[ic] * self.dt**1.5 * beta ) sp_start += num # Enforce boundary condition self.enforce_bc(ptcls) acc_old = ptcls.acc.copy() self.update_accelerations(ptcls) sp_start = 0 sp_end = 0 for ic, num in enumerate(self.species_num): sp_end += num ptcls.vel[sp_start:sp_end, : self.dimensions] = ( self.c1 * self.c2 * ptcls.vel[sp_start:sp_end, : self.dimensions] + 0.5 * self.c2 * self.dt * (ptcls.acc[sp_start:sp_end, : self.dimensions] + acc_old[sp_start:sp_end, : self.dimensions]) + self.c2 * self.sigma[ic] * sqrt(self.dt) * beta ) sp_start += num
[docs] def verlet(self, ptcls): """ Update particles' class based on velocity verlet algorithm. More information can be found here: https://en.wikipedia.org/wiki/Verlet_integration or on the Sarkas website. Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. """ # First half step velocity update ptcls.vel += 0.5 * ptcls.acc * self.dt # Full step position update ptcls.pos += ptcls.vel * self.dt # Enforce boundary condition self.enforce_bc(ptcls) # Compute total potential energy and acceleration for second half step velocity update self.update_accelerations(ptcls) # Second half step velocity update ptcls.vel += 0.5 * ptcls.acc * self.dt
[docs] def magnetic_helpers(self, coefficient): """Calculate the trigonometric functions of the magnetic integrators. Parameters ---------- coefficient: float Timestep coefficient. Notes ----- This is useful for the Leapfrog magnetic algorithm and future Forest-Ruth and MacLachlan algorithms. """ theta = self.omega_c * self.dt * coefficient self.sdt = sin(theta) self.cdt = cos(theta) self.ccodt = 1.0 - self.cdt self.ssodt = 1.0 - self.sdt / theta
[docs] def magnetic_verlet_zdir(self, ptcls): """ Update particles' class based on velocity verlet method in the case of a constant magnetic field along the :math:`z` axis. For more info see eq. (78) of Ref. :cite:`Chin2008` Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. Returns ------- potential_energy : float Total potential energy. Notes ----- This integrator is faster than `magnetic_verlet` but valid only for a magnetic field in the :math:`z`-direction. This is the preferred choice in this case. """ # First half step of velocity update # # Magnetic rotation x - velocity # (B x v)_x = -v_y, (B x B x v)_x = -v_x self.v_B[:, 0] = ptcls.vel[:, 1] * self.sdt[:, 0] + ptcls.vel[:, 0] * self.cdt[:, 0] # Magnetic rotation y - velocity # (B x v)_y = v_x, (B x B x v)_y = -v_y self.v_B[:, 1] = -ptcls.vel[:, 0] * self.sdt[:, 0] + ptcls.vel[:, 1] * self.cdt[:, 1] # Magnetic + Const force field x - velocity # (B x a)_x = -a_y, (B x B x a)_x = -a_x self.v_F[:, 0] = ( self.ccodt[:, 1] / self.omega_c[:, 1] * ptcls.acc[:, 1] + self.sdt[:, 0] / self.omega_c[:, 0] * ptcls.acc[:, 0] ) # Magnetic + Const force field y - velocity # (B x a)_y = a_x, (B x B x a)_y = -a_y self.v_F[:, 1] = ( -self.ccodt[:, 0] / self.omega_c[:, 0] * ptcls.acc[:, 0] + self.sdt[:, 1] / self.omega_c[:, 1] * ptcls.acc[:, 1] ) ptcls.vel[:, 0] = self.v_B[:, 0] + self.v_F[:, 0] ptcls.vel[:, 1] = self.v_B[:, 1] + self.v_F[:, 1] ptcls.vel[:, 2] += 0.5 * self.dt * ptcls.acc[:, 2] # Position update ptcls.pos += ptcls.vel * self.dt # Enforce boundary condition self.enforce_bc(ptcls) # Compute total potential energy and acceleration for second half step velocity update potential_energy = self.update_accelerations(ptcls) # # Magnetic rotation x - velocity # (B x v)_x = -v_y, (B x B x v)_x = -v_x self.v_B[:, 0] = ptcls.vel[:, 1] * self.sdt[:, 0] + ptcls.vel[:, 0] * self.cdt[:, 0] # Magnetic rotation y - velocity # (B x v)_y = v_x, (B x B x v)_y = -v_y self.v_B[:, 1] = -ptcls.vel[:, 0] * self.sdt[:, 0] + ptcls.vel[:, 1] * self.cdt[:, 1] # Magnetic + Const force field x - velocity # (B x a)_x = -a_y, (B x B x a)_x = -a_x self.v_F[:, 0] = ( self.ccodt[:, 1] / self.omega_c[:, 1] * ptcls.acc[:, 1] + self.sdt[:, 0] / self.omega_c[:, 0] * ptcls.acc[:, 0] ) # Magnetic + Const force field y - velocity # (B x a)_y = a_x, (B x B x a)_y = -a_y self.v_F[:, 1] = ( -self.ccodt[:, 0] / self.omega_c[:, 0] * ptcls.acc[:, 0] + self.sdt[:, 1] / self.omega_c[:, 1] * ptcls.acc[:, 1] ) ptcls.vel[:, 0] = self.v_B[:, 0] + self.v_F[:, 0] ptcls.vel[:, 1] = self.v_B[:, 1] + self.v_F[:, 1] ptcls.vel[:, 2] += 0.5 * self.dt * ptcls.acc[:, 2] return potential_energy
[docs] def magnetic_verlet(self, ptcls): """ Update particles' class based on velocity verlet method in the case of an arbitrary direction of the constant magnetic field. For more info see eq. (78) of Ref. :cite:`Chin2008` Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. Returns ------- potential_energy : float Total potential energy. Notes ----- :cite:`Chin2008` equations are written for a negative charge. This allows him to write :math:`\\dot{\\mathbf v} = \\omega_c \\hat{B} \\times \\mathbf v`. In the case of positive charges we will have :math:`\\dot{\\mathbf v} = - \\omega_c \\hat{B} \\times \\mathbf v`. Hence the reason of the different signs in the formulas below compared to Chin's. Warnings -------- This integrator is valid for a magnetic field in an arbitrary direction. However, while the integrator works for an arbitrary direction, methods in :mod:`sarkas.tools.observables` work only for a magnetic field in the :math:`z` - direction. Hence, if you choose to use this integrator remember to change your physical observables. """ # Calculate the cross products b_cross_v = cross(self.magnetic_field_uvector, ptcls.vel) b_cross_b_cross_v = cross(self.magnetic_field_uvector, b_cross_v) b_cross_a = cross(self.magnetic_field_uvector, ptcls.acc) b_cross_b_cross_a = cross(self.magnetic_field_uvector, b_cross_a) # First half step of velocity update ptcls.vel += -self.sdt * b_cross_v + self.ccodt * b_cross_b_cross_v ptcls.vel += ( 0.5 * ptcls.acc * self.dt - self.ccodt / self.omega_c * b_cross_a + 0.5 * self.dt * self.ssodt * b_cross_b_cross_a ) # Position update ptcls.pos += ptcls.vel * self.dt # Enforce boundary condition self.enforce_bc(ptcls) # Compute total potential energy and acceleration for second half step velocity update potential_energy = self.update_accelerations(ptcls) # Re-calculate the cross products b_cross_v = cross(self.magnetic_field_uvector, ptcls.vel) b_cross_b_cross_v = cross(self.magnetic_field_uvector, b_cross_v) b_cross_a = cross(self.magnetic_field_uvector, ptcls.acc) b_cross_b_cross_a = cross(self.magnetic_field_uvector, b_cross_a) # Second half step velocity update ptcls.vel += -self.sdt * b_cross_v + self.ccodt * b_cross_b_cross_v ptcls.vel += ( 0.5 * ptcls.acc * self.dt - self.ccodt / self.omega_c * b_cross_a + 0.5 * self.dt * self.ssodt * b_cross_b_cross_a ) return potential_energy
[docs] def magnetic_boris_zdir(self, ptcls): """ Update particles' class using the Boris algorithm in the case of a constant magnetic field along the :math:`z` axis. For more info see eqs. (80) - (81) of Ref. :cite:`Chin2008` Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. Returns ------- potential_energy : float Total potential energy. """ # First half step of velocity update: Apply exp(dt * V_F / 2) ptcls.vel += 0.5 * ptcls.acc * self.dt # Rotate: Apply exp( dt * V) # B cross v self.v_B[:, 0] = -self.sdt[:, 1] * ptcls.vel[:, 1] self.v_B[:, 1] = self.sdt[:, 0] * ptcls.vel[:, 0] # B cross B cross v self.v_B[:, 0] -= self.ccodt[:, 0] * ptcls.vel[:, 0] self.v_B[:, 1] -= self.ccodt[:, 1] * ptcls.vel[:, 1] # Update velocities ptcls.vel[:, :2] += self.v_B[:, :2] # Second Acceleration half step: Apply exp(dt * V_F / 2) ptcls.vel += 0.5 * ptcls.acc * self.dt # Full step position update ptcls.pos += ptcls.vel * self.dt # Enforce boundary condition self.enforce_bc(ptcls) # Compute total potential energy and acceleration for second half step velocity update potential_energy = self.update_accelerations(ptcls) return potential_energy
[docs] def magnetic_boris(self, ptcls): """ Update particles' class using the Boris algorithm in the case of a constant magnetic field along the :math:`z` axis. For more info see eqs. (80) - (81) of Ref. :cite:`Chin2008` Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. Returns ------- potential_energy : float Total potential energy. """ # First half step of velocity update: Apply exp(eV_F/2) ptcls.vel += 0.5 * ptcls.acc * self.dt # Rotate: Apply exp( dt * V) # B cross v b_cross_v = cross(self.magnetic_field_uvector, ptcls.vel) # B cross B cross v b_cross_b_cross_v = cross(self.magnetic_field_uvector, b_cross_v) ptcls.vel += self.sdt * b_cross_v + self.ccodt * b_cross_b_cross_v # Second Acceleration half step: Apply exp(dt * V_F / 2) ptcls.vel += 0.5 * ptcls.acc * self.dt # Full step position update ptcls.pos += ptcls.vel * self.dt # Periodic boundary condition enforce_pbc(ptcls.pos, ptcls.pbc_cntr, self.box_lengths) # Compute total potential energy and acceleration for second half step velocity update potential_energy = self.update_accelerations(ptcls) return potential_energy
[docs] def magnetic_pos_verlet_zdir(self, ptcls): """ Update particles' class based on position verlet method in the case of a constant magnetic field along the :math:`z` axis. For more info see eq. (79) of Ref. :cite:`Chin2008` Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. Returns ------- potential_energy : float Total potential energy. Notes ----- This integrator is faster than `magnetic_verlet` but valid only for a magnetic field in the :math:`z`-direction. This is the preferred choice in this case. """ # Position update ptcls.pos += 0.5 * ptcls.vel * self.dt # Enforce boundary condition self.enforce_bc(ptcls) # Compute total potential energy and acceleration for second half step velocity update potential_energy = self.update_accelerations(ptcls) # First half step of velocity update # # Magnetic rotation x - velocity # (B x v)_x = -v_y, (B x B x v)_x = -v_x self.v_B[:, 0] = ptcls.vel[:, 1] * self.sdt[:, 0] + ptcls.vel[:, 0] * self.cdt[:, 0] # Magnetic rotation y - velocity # (B x v)_y = v_x, (B x B x v)_y = -v_y self.v_B[:, 1] = -ptcls.vel[:, 0] * self.sdt[:, 0] + ptcls.vel[:, 1] * self.cdt[:, 1] # Magnetic + Const force field x - velocity # (B x a)_x = -a_y, (B x B x a)_x = -a_x self.v_F[:, 0] = ( self.ccodt[:, 1] / self.omega_c[:, 1] * ptcls.acc[:, 1] + self.sdt[:, 0] / self.omega_c[:, 0] * ptcls.acc[:, 0] ) # Magnetic + Const force field y - velocity # (B x a)_y = a_x, (B x B x a)_y = -a_y self.v_F[:, 1] = ( -self.ccodt[:, 0] / self.omega_c[:, 0] * ptcls.acc[:, 0] + self.sdt[:, 1] / self.omega_c[:, 1] * ptcls.acc[:, 1] ) ptcls.vel[:, 0] = self.v_B[:, 0] + self.v_F[:, 0] ptcls.vel[:, 1] = self.v_B[:, 1] + self.v_F[:, 1] ptcls.vel[:, 2] += self.dt * ptcls.acc[:, 2] # Position update ptcls.pos += 0.5 * ptcls.vel * self.dt # Enforce boundary condition self.enforce_bc(ptcls) return potential_energy
[docs] def magnetic_pos_verlet(self, ptcls): """ Update particles' class based on position verlet method in the case of an arbitrary direction of the constant magnetic field. For more info see eq. (79) of Ref. :cite:`Chin2008` Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. Returns ------- potential_energy : float Total potential energy. Notes ----- :cite:`Chin2008` equations are written for a negative charge. This allows him to write :math:`\\dot{\\mathbf v} = \\omega_c \\hat{B} \\times \\mathbf v`. In the case of positive charges we will have :math:`\\dot{\\mathbf v} = - \\omega_c \\hat{B} \\times \\mathbf v`. Hence the reason of the different signs in the formulas below compared to Chin's. Warnings -------- This integrator is valid for a magnetic field in an arbitrary direction. However, while the integrator works for an arbitrary direction, methods in :mod:`sarkas.tools.observables` work only for a magnetic field in the :math:`z` - direction. Hence, if you choose to use this integrator remember to change your physical observables. """ # Half position update ptcls.pos += ptcls.vel * self.dt # Enforce boundary condition self.enforce_bc(ptcls) # Compute total potential energy and acceleration for second half step velocity update potential_energy = self.update_accelerations(ptcls) # Calculate the cross products b_cross_v = cross(self.magnetic_field_uvector, ptcls.vel) b_cross_b_cross_v = cross(self.magnetic_field_uvector, b_cross_v) b_cross_a = cross(self.magnetic_field_uvector, ptcls.acc) b_cross_b_cross_a = cross(self.magnetic_field_uvector, b_cross_a) # First half step of velocity update ptcls.vel += -self.sdt * b_cross_v + self.ccodt * b_cross_b_cross_v ptcls.vel += ( ptcls.acc * self.dt - self.ccodt / self.omega_c * b_cross_a + self.dt * self.ssodt * b_cross_b_cross_a ) # Second half position update ptcls.pos += ptcls.vel * self.dt # Enforce boundary condition self.enforce_bc(ptcls) return potential_energy
[docs] def cyclotronic_zdir(self, ptcls): """ Update particles' class using the cyclotronic algorithm in the case of a constant magnetic field along the :math:`z` axis. For more info see eqs. (16) - (17) of Ref. :cite:`Patacchini2009` Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. Returns ------- potential_energy : float Total potential energy. """ # Drift half step # Rotate Positions ptcls.pos[:, 0] += ( ptcls.vel[:, 0] * self.sdt[:, 0] / self.omega_c[:, 0] + ptcls.vel[:, 1] * self.ccodt[:, 1] / self.omega_c[:, 1] ) ptcls.pos[:, 1] += ( ptcls.vel[:, 1] * self.sdt[:, 1] / self.omega_c[:, 1] - ptcls.vel[:, 0] * self.ccodt[:, 0] / self.omega_c[:, 0] ) ptcls.pos[:, 2] += 0.5 * ptcls.vel[:, 2] * self.dt # Enforce boundary condition self.enforce_bc(ptcls) # Create rotated velocities self.v_B[:, 0] = self.cdt[:, 0] * ptcls.vel[:, 0] + self.sdt[:, 1] * ptcls.vel[:, 1] self.v_B[:, 1] = self.cdt[:, 1] * ptcls.vel[:, 1] - self.sdt[:, 0] * ptcls.vel[:, 0] ptcls.vel[:, :2] = self.v_B[:, :2].copy() # Compute total potential energy and accelerations potential_energy = self.update_accelerations(ptcls) # Kick full step ptcls.vel += ptcls.acc * self.dt # Drift half step # Rotate Positions ptcls.pos[:, 0] += ( ptcls.vel[:, 0] * self.sdt[:, 0] / self.omega_c[:, 0] + ptcls.vel[:, 1] * self.ccodt[:, 1] / self.omega_c[:, 1] ) ptcls.pos[:, 1] += ( ptcls.vel[:, 1] * self.sdt[:, 1] / self.omega_c[:, 1] - ptcls.vel[:, 0] * self.ccodt[:, 0] / self.omega_c[:, 0] ) ptcls.pos[:, 2] += 0.5 * ptcls.vel[:, 2] * self.dt # Enforce boundary condition self.enforce_bc(ptcls) # Create rotated velocities self.v_B[:, 0] = self.cdt[:, 0] * ptcls.vel[:, 0] + self.sdt[:, 1] * ptcls.vel[:, 1] self.v_B[:, 1] = self.cdt[:, 1] * ptcls.vel[:, 1] - self.sdt[:, 0] * ptcls.vel[:, 0] # Update final velocities ptcls.vel[:, :2] = self.v_B[:, :2].copy() return potential_energy
[docs] def cyclotronic(self, ptcls): """ Update particles' class using the cyclotronic algorithm in the case of a constant magnetic field along the :math:`z` axis. For more info see eqs. (16) - (17) of Ref. :cite:`Patacchini2009` Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. Returns ------- potential_energy : float Total potential energy. """ # Drift half step # Calculate the cross products b_cross_v = cross(self.magnetic_field_uvector, ptcls.vel) b_cross_b_cross_v = cross(self.magnetic_field_uvector, b_cross_v) # Rotate Positions ptcls.pos += ( 0.5 * ptcls.vel * self.dt - self.ccodt * b_cross_v / self.omega_c + 0.5 * self.dt * self.ssodt * b_cross_b_cross_v ) # Enforce boundary condition self.enforce_bc(ptcls) # First half step of velocity update ptcls.vel += -self.sdt * b_cross_v + self.ccodt * b_cross_b_cross_v # Compute total potential energy and accelerations potential_energy = self.update_accelerations(ptcls) # Kick full step ptcls.vel += ptcls.acc * self.dt # Drift half step # Calculate the cross products b_cross_v = cross(self.magnetic_field_uvector, ptcls.vel) b_cross_b_cross_v = cross(self.magnetic_field_uvector, b_cross_v) # Rotate Positions ptcls.pos += ( 0.5 * ptcls.vel * self.dt - self.ccodt * b_cross_v / self.omega_c + 0.5 * self.dt * self.ssodt * b_cross_b_cross_v ) # Enforce boundary condition self.enforce_bc(ptcls) # Second half step of velocity update ptcls.vel += -self.sdt * b_cross_v + self.ccodt * b_cross_b_cross_v return potential_energy
[docs] def thermostate(self, ptcls): """ Update particles' velocities according to the chosen thermostat Parameters ---------- ptcls : :class:`sarkas.particles.Particles` Particles' data. """ _, T = ptcls.kinetic_temperature() berendsen(ptcls.vel, self.species_temperatures, T, self.species_num, self.thermalization_rate)
[docs] def periodic_bc(self, ptcls): """ Applies periodic boundary conditions by calling enforce_pbc Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. """ enforce_pbc(ptcls.pos, ptcls.pbc_cntr, self.box_lengths)
[docs] def absorbing_bc(self, ptcls): """ Applies absorbing boundary conditions by calling enforce_abc Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. """ enforce_abc(ptcls.pos, ptcls.vel, ptcls.acc, ptcls.charges, self.box_lengths)
[docs] def open_bc(self, ptcls): """ Applies open boundary conditions. Basically it does nothing. pass Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. """ pass
[docs] def reflecting_bc(self, ptcls): """ Applies reflective boundary conditions by calling enforce_rbc Parameters ---------- ptcls: :class:`sarkas.particles.Particles` Particles data. """ enforce_rbc(ptcls.pos, ptcls.vel, self.box_lengths, self.dt)
[docs] def pretty_print(self): """Print integrator and thermostat information in a user-friendly way.""" if self.thermalization: print("\nTHERMOSTAT: ") print(f"Type: {self.thermostat_type}") print(f"First thermostating timestep, i.e. thermalization_timestep = {self.thermalization_timestep}") print(f"Berendsen parameter tau: {self.berendsen_tau:.3f} [timesteps]") print(f"Berendsen relaxation rate: {self.thermalization_rate:.3f} [1/timesteps] ") print("Thermostating temperatures: ") for i, (t, t_ev) in enumerate(zip(self.thermostate_temperatures, self.thermostate_temperatures_eV)): print(f"Species ID {i}: T_eq = {t:.6e} [K] = {t_ev:.6e} [eV]") print("\nINTEGRATOR: ") print(f"Equilibration Integrator Type: {self.equilibration_type}") if self.magnetized: print(f"Magnetization Integrator Type: {self.magnetization_type}") print(f"Production Integrator Type: {self.production_type}") wp_tot = norm(self.species_plasma_frequencies) wp_dt = wp_tot * self.dt print(f"Time step = {self.dt:.6e} [s]") print(f"Total plasma frequency = {wp_tot:.6e} [rad/s]") print(f"w_p dt = {wp_dt:.4f} ~ 1/{int(1.0 / wp_dt)}") if self.potential_type == "qsp": wp_e = self.species_plasma_frequencies[0] wp_ions = norm(self.species_plasma_frequencies[1:]) print(f"e plasma frequency = {wp_e:.6e} [rad/s]") print(f"total ion plasma frequency = {wp_ions:.6e} [rad/s]") print(f"w_pe dt = {self.dt * wp_e:.4f} ~ 1/{int(1.0 / (self.dt * wp_e))}") print(f"w_pi dt = {self.dt * wp_ions:.4f} ~ 1/{int(1.0 / (self.dt * wp_ions))}") elif self.potential_type == "lj": print(f"The plasma frequency is defined as w_p = sqrt( epsilon / (sigma^2 * mass) )") if self.magnetized: high_wc_dt = abs(self.species_cyclotron_frequencies).max() * self.dt low_wc_dt = abs(self.species_cyclotron_frequencies).min() * self.dt if high_wc_dt > low_wc_dt: print(f"Highest w_c dt = {high_wc_dt:2.4f} = {high_wc_dt / pi:.4f} pi") print(f"Smallest w_c dt = {low_wc_dt:2.4f} = {low_wc_dt / pi:.4f} pi") else: print(f"w_c dt = {high_wc_dt:2.4f} = {high_wc_dt / pi:.4f} pi") if self.equilibration_type == "langevin" or self.production_type == "langevin": print(f"langevin_gamma * dt = {self.langevin_gamma * self.dt:.4e}") print(f"langevin_gamma / wp = {self.langevin_gamma / wp_tot:.4e}")
@jit(void(float64[:, :], float64[:], float64[:], int64[:], float64), nopython=True) def berendsen(vel, T_desired, T, species_np, tau): """ Numba'd function to update particle velocity based on Berendsen thermostat :cite:`Berendsen1984`. Parameters ---------- vel : numpy.ndarray Particles' velocities to rescale. T_desired : numpy.ndarray Target temperature of each species. T : numpy.ndarray Instantaneous temperature of each species. species_np : numpy.ndarray Number of each species. tau : float Scale factor. """ # if it < therm_timestep: # fact = sqrt(T_desired / T) # else: # fact = sqrt(1.0 + (T_desired / T - 1.0) * tau) # eq.(11) # branchless programming fact = sqrt(1.0 + (T_desired / T - 1.0) * tau) species_start = 0 species_end = 0 for i, num in enumerate(species_np): species_end += num vel[species_start:species_end, :] *= fact[i] species_start += num @jit(void(float64[:, :], float64[:, :], float64[:]), nopython=True) def enforce_pbc(pos, cntr, box_vector) -> None: """ Numba'd function to enforce periodic boundary conditions. Parameters ---------- pos : numpy.ndarray Particles' positions. cntr : numpy.ndarray Counter for the number of times each particle get folded back into the main simulation box box_vector : numpy.ndarray Box Dimensions. """ # Loop over all particles for p in arange(pos.shape[0]): for d in arange(pos.shape[1]): # If particle is outside of box in positive direction, wrap to negative side if pos[p, d] > box_vector[d]: pos[p, d] -= box_vector[d] cntr[p, d] += 1 # If particle is outside of box in negative direction, wrap to positive side if pos[p, d] < 0.0: pos[p, d] += box_vector[d] cntr[p, d] -= 1 @jit(void(float64[:, :], float64[:, :], float64[:, :], float64[:], float64[:]), nopython=True) def enforce_abc(pos, vel, acc, charges, box_vector) -> None: """ Numba'd function to enforce absorbing boundary conditions. Parameters ---------- pos: numpy.ndarray Particles' positions. vel : numpy.ndarray Particles' velocities. acc : numpy.ndarray Particles' accelerations. charges : numpy.ndarray Charge of each particle. Shape = (``total_num_ptcls``). box_vector: numpy.ndarray Box Dimensions. """ # Loop over all particles for p in arange(pos.shape[0]): for d in arange(pos.shape[1]): # If particle is outside of box in positive direction, remove charge, velocity and acceleration if pos[p, d] >= box_vector[d]: pos[p, d] = box_vector[d] vel[p, :] = zeros(3) acc[p, :] = zeros(3) charges[p] = 0.0 # If particle is outside of box in negative direction, remove charge, velocity and acceleration if pos[p, d] <= 0.0: pos[p, d] = 0.0 vel[p, :] = zeros(3) acc[p, :] = zeros(3) charges[p] = 0.0 @jit(void(float64[:, :], float64[:, :], float64[:], float64), nopython=True) def enforce_rbc(pos, vel, box_vector, dt) -> None: """ Numba'd function to enforce reflecting boundary conditions. Parameters ---------- pos: numpy.ndarray Particles' positions. vel : numpy.ndarray Particles' velocities. box_vector: numpy.ndarray Box Dimensions. dt : float Timestep. """ # Loop over all particles for p in arange(pos.shape[0]): for d in arange(pos.shape[1]): # If particle is outside of box in positive direction, wrap to negative side if pos[p, d] > box_vector[d] or pos[p, d] < 0.0: # Revert velocity vel[p, d] *= -1.0 # Restore previous position assuming verlet algorithm pos[p, d] += vel[p, d] * dt # @jit(void(float64[:, :], int64[:], float64[:]), nopython=True) # def remove_drift(vel, nums, masses) -> None: # """ # Numba'd function to enforce conservation of total linear momentum. # It updates :attr:`sarkas.particles.Particles.vel`. # # Parameters # ---------- # vel: numpy.ndarray # Particles' velocities. # # nums: numpy.ndarray # Number of particles of each species. # # masses: numpy.ndarray # Mass of each species. # # """ # # P = zeros((len(nums), vel.shape[1])) # species_start = 0 # for ic in range(len(nums)): # species_end = species_start + nums[ic] # P[ic, :] = vel[species_start:species_end, :].sum(axis=0) * masses[ic] # species_start = species_end # # if P.sum(axis=0).any() > 1e-40: # # Remove tot momentum # species_start = 0 # for ic in range(len(nums)): # species_end = species_start + nums[ic] # vel[species_start:species_end, :] -= P[ic, :] / (float(nums[ic]) * masses[ic]) # species_start = species_end