Source code for friction_tools

#! /usr/bin/env python
# coding=utf-8
"""A module containing routines for setting up simple friction simulations.
"""

# get Pysic - this is the package that handles atomic interactions
# see http://thynnine.github.io/pysic/
import pysic

# get some components of ASE - this is the package that handles dynamics
# see https://wiki.fysik.dtu.dk/ase/
from ase.lattice.compounds import Rocksalt
#from ase.structure import bulk
from ase.lattice import bulk
from ase.md.velocitydistribution import MaxwellBoltzmannDistribution
from ase.md.verlet import VelocityVerlet
from ase.md.npt import NPT
from ase.md.langevin import Langevin
from ase.md.nvtberendsen import NVTBerendsen
from ase.optimize import BFGSLineSearch
from ase.io.trajectory import PickleTrajectory
from ase.constraints import FixAtoms
from ase.lattice.hexagonal import Graphite
import ase.io
from ase import Atoms
from ase import units

# get numeric tools from NumPy - see http://scipy.org
import numpy as np
import numpy.random as nr

# get Python tools - these are part of the Python default library
import math
import time
import copy


# get plotting tools
try:
    import matplotlib.pyplot as plt
except:
    print "MatPlotLib not found, plotting will not work"


[docs]def set_random_number_seed(seed=5801): """Initialize random number generator (RNG). By default, the RNG is always given the same seed so that simulations are reproducible. However, if you would like to rerun simulations with different random sequences, give a new random seed here. This affects :meth:`set_initial_temperature` and :meth:`create_dynamics` / :math:`run_simulation` if Langevin dynamics are chosen. Parameters: seed: integer a new RNG seed """ nr.seed(seed)
set_random_number_seed() # mod the Langevin dynamics class from ASE from ase.parallel import world from ase.md.md import MolecularDynamics import sys class LangevinMod(Langevin): def __init__(self, atoms, timestep, temperature, friction, fixcm=False, trajectory=None, logfile=None, loginterval=1, communicator=world, applyArray=None): """A copy of the Langevin dynamics class from ASE in case modifications need to be made. In Langevin dynamics, a friction force is applied to moving atoms to prevent them from accumulating too much kinetic energy. This will drive the temperature to zero over time, and so it is balanced by a randomly fluctuating force that affects each atom independently. The strength of this random force will determine the temperature of the system in equilibrium. """ MolecularDynamics.__init__(self, atoms, timestep, trajectory, logfile, loginterval) self.temp = temperature self.frict = friction self.fixcm = fixcm # will the center of mass be held fixed? self.communicator = communicator self.updatevars() self.apply = applyArray def updatevars(self): dt = self.dt # If the friction is an array some other constants must be arrays too. self._localfrict = hasattr(self.frict, 'shape') lt = self.frict * dt masses = self.masses sdpos = dt * np.sqrt(self.temp / masses.reshape(-1) * (2.0/3.0 - 0.5 * lt) * lt) sdpos.shape = (-1, 1) sdmom = np.sqrt(self.temp * masses.reshape(-1) * 2.0 * (1.0 - lt) * lt) sdmom.shape = (-1, 1) pmcor = np.sqrt(3.0)/2.0 * (1.0 - 0.125 * lt) cnst = np.sqrt((1.0 - pmcor) * (1.0 + pmcor)) act0 = 1.0 - lt + 0.5 * lt * lt act1 = (1.0 - 0.5 * lt + (1.0/6.0) * lt * lt) act2 = 0.5 - (1.0/6.0) * lt + (1.0/24.0) * lt * lt c1 = act1 * dt / masses.reshape(-1) c1.shape = (-1, 1) c2 = act2 * dt * dt / masses.reshape(-1) c2.shape = (-1, 1) c3 = (act1 - act2) * dt c4 = act2 * dt del act1, act2 if self._localfrict: # If the friction is an array, so are these act0.shape = (-1, 1) c3.shape = (-1, 1) c4.shape = (-1, 1) pmcor.shape = (-1, 1) cnst.shape = (-1, 1) self.sdpos = sdpos self.sdmom = sdmom self.c1 = c1 self.c2 = c2 self.act0 = act0 self.c3 = c3 self.c4 = c4 self.pmcor = pmcor self.cnst = cnst self.natoms = self.atoms.get_number_of_atoms() def step(self, f): atoms = self.atoms p = self.atoms.get_momenta() random1 = nr.standard_normal(size=(len(atoms), 3)) random2 = nr.standard_normal(size=(len(atoms), 3)) if self.communicator is not None: self.communicator.broadcast(random1, 0) self.communicator.broadcast(random2, 0) rrnd = self.sdpos * random1 prnd = (self.sdmom * self.pmcor * random1 + self.sdmom * self.cnst * random2) if self.fixcm: rrnd = rrnd - np.sum(rrnd, 0) / len(atoms) prnd = prnd - np.sum(prnd, 0) / len(atoms) rrnd *= np.sqrt(self.natoms / (self.natoms - 1.0)) prnd *= np.sqrt(self.natoms / (self.natoms - 1.0)) atoms.set_positions(atoms.get_positions() + self.c1 * p + self.c2 * f + rrnd) p *= self.act0 p += self.c3 * f + prnd atoms.set_momenta(p) f = atoms.get_forces() atoms.set_momenta(atoms.get_momenta() + self.c4 * f) return f
[docs]class FixPositions: """A constraint class for fixing atomic positions """ def __init__(self, indices, fixed): self.indices = indices self.fixed = fixed def adjust_positions(self, oldpositions, newpositions): for index in self.indices: for i in range(3): if self.fixed[i]: newpositions[index,i] = oldpositions[index,i] def adjust_forces(self, positions, forces): for index in self.indices: for i in range(3): if self.fixed[i]: forces[index,i] = 0.0 def copy(self): return copy.deepcopy(self)
[docs]class FixVelocities: """A constraint class for fixing atomic velocities """ def __init__(self, indices, velocity, dt, fixed): self.indices = indices self.fixed = fixed self.step = np.array(velocity, dtype=float)*dt self.velocity = np.array(velocity) def adjust_positions(self, oldpositions, newpositions): for index in self.indices: for i in range(3): if self.fixed[i]: newpositions[index,i] = oldpositions[index,i] + self.step[i] def adjust_forces(self, positions, forces): for index in self.indices: for i in range(3): if self.fixed[i]: forces[index,i] = 0.0 def copy(self): return copy.deepcopy(self)
[docs]class FixPlane: """A constraint class for fixing atomic positions in a plane """ def __init__(self, indices, normal): self.indices = indices self.normal = np.array(normal) / math.sqrt(np.dot(normal,normal)) def adjust_positions(self, oldpositions, newpositions): for index in self.indices: newpositions[index] -= self.normal * \ np.dot(newpositions[index] - oldpositions[index], self.normal) def adjust_forces(self, positions, forces): for index in self.indices: forces[index] -= self.normal * np.dot(forces[index], self.normal) def copy(self): return copy.deepcopy(self)
[docs]class FixLine: """A constraint class for fixing atomic positions on a line """ def __init__(self, indices, direction): self.indices = indices self.direction = np.array(direction) / math.sqrt(np.dot(direction,direction)) def adjust_positions(self, oldpositions, newpositions): for index in self.indices: tangent = np.dot(newpositions[index] - oldpositions[index], self.direction) newpositions[index] = oldpositions[index] + tangent * self.direction def adjust_forces(self, positions, forces): for index in self.indices: tangent = np.dot(forces[index], self.direction) forces[index] = tangent * self.direction def copy(self): return copy.deepcopy(self)
[docs]class FrictionSimulation: """A class defining a simulation environment. The class contains convenient methods for setting up a simulation. """ # Default values timestep = 2.0 cell_width = 10.0 def __init__(self): self.dynamics = None self.system = None self.trajectory = None self.calc = None self.old_positions = None self.prev_positions = None self.prev_forces = None self.work = np.zeros(0,dtype=float) self.timestep = FrictionSimulation.timestep self.cell_width = FrictionSimulation.cell_width self.system = ase.Atoms() self.calc = pysic.Pysic() self.system.set_calculator(self.calc) pysic.utility.error.set_warning_level(1) def __del__(self): if not self.trajectory is None: self.trajectory.close()
[docs] def create_graphite(self): """Creates a graphite structure. The slab is 2D infinite in xy-plane, its surfaces exposed in the z-direction. The structure is FCC, see e.g. http://en.wikipedia.org/wiki/Face_Centered_Cubic Parameters: element: string the chemical symbol for the atoms of the slab, e.g., 'Au' bottom_z: real number the z-coordinate of the lower surface of the slab top_z: real number the z-coordinate of the top surface of the slab xy_cells: integer the number of lattice cells to be created in x and y directions - as the width of the simulation box is determined by the :class:`FrictionSimulation` ('cell_width'), this will determine the lattice constant z_cells: integer the number of lattice cells to be created in z direction """ graphite = Graphite( symbol='C', latticeconstant={'a': 2.46, 'c': 4*math.sqrt(3)/3*2.46}, size=(5, 5, 1) ) # from ase.visualize import view # view(graphite) self.system.set_cell(graphite.get_cell()) self.system.set_pbc([True, True, False]) self.system += graphite
[docs] def create_graphite2(self): """Creates a graphite structure. The slab is 2D infinite in xy-plane, its surfaces exposed in the z-direction. The structure is FCC, see e.g. http://en.wikipedia.org/wiki/Face_Centered_Cubic Parameters: element: string the chemical symbol for the atoms of the slab, e.g., 'Au' bottom_z: real number the z-coordinate of the lower surface of the slab top_z: real number the z-coordinate of the top surface of the slab xy_cells: integer the number of lattice cells to be created in x and y directions - as the width of the simulation box is determined by the :class:`FrictionSimulation` ('cell_width'), this will determine the lattice constant z_cells: integer the number of lattice cells to be created in z direction """ # Create top part a = 2.46 d = math.sqrt(3)/3*a positions = np.array([ [0.0, 0.0, 0.0], [1.0/2.0*d, math.sqrt(3)/2.0*d, 0.0], [3.0/2.0*d, math.sqrt(3)/2.0*d, 0.0], [2.0*d, 0.0, 0.0] ]) cell = np.array( [3*d, math.sqrt(3)*d, 3] ) top = ase.Atoms(symbols="CCCC", positions=positions, cell=cell) top *= [3, 3, 1] # Create bottom part bottom = ase.Atoms(symbols="NNNN", positions=positions, cell=cell) bottom *= [3, 3, 1] bottom.translate([d, 0, 3]) self.system += top self.system += bottom self.system.set_cell(top.get_cell()) self.system.set_pbc([True, True, False])
# print(self.system.get_cell()) # from ase.visualize import view # view(self.system)
[docs] def create_slab(self, element, bottom_z=0.0, top_z=None, z_cells=3, xy_cells=4): """Creates a slab of atoms in the FCC (face-centered cubic) lattice structure. The slab is 2D infinite in xy-plane, its surfaces exposed in the z-direction. The structure is FCC, see e.g. http://en.wikipedia.org/wiki/Face_Centered_Cubic Parameters: element: string the chemical symbol for the atoms of the slab, e.g., 'Au' bottom_z: real number the z-coordinate of the lower surface of the slab top_z: real number the z-coordinate of the top surface of the slab xy_cells: integer the number of lattice cells to be created in x and y directions - as the width of the simulation box is determined by the :class:`FrictionSimulation` ('cell_width'), this will determine the lattice constant z_cells: integer the number of lattice cells to be created in z direction """ lattice_constant = self.cell_width / xy_cells print "Lattice constant of the created slab: {}".format(lattice_constant) slab = bulk(element, 'fcc', a=lattice_constant, cubic=True)*(xy_cells,xy_cells,z_cells) if top_z is None: slab.translate([0.0, 0.0, bottom_z]) elif bottom_z == 0.0: slab.translate([0.0, 0.0, top_z-lattice_constant*(z_cells-0.5)]) else: print "Only give either the top or the bottom of the slab as an argument." print "Setting the z-position accirding to bottom_z = {bz}".format(bz=str(bottom_z)) slab.translate([0.0, 0.0, bottom_z]) self.system += slab self.system.set_cell([[lattice_constant*xy_cells,0.0,0.0], [0.0,lattice_constant*xy_cells,0.0], [0.0,0.0,100.0]]) self.system.set_pbc([True, True, False])
[docs] def create_random_atoms(self,number,element,bottom_z,top_z, minimum_distance=2.0): """Adds the given number of atoms randomly in a given volume. The atoms are added randomly and for each added atom it is checked that it is not too close to previously added atoms. If you try to insert too many atoms, there will not be enough room and the algorithm stops after failing for long enough. Parameters: number: integer the number of atoms to add element: string the chemical symbol of the atoms, e.g., 'C' bottom_z: real number the minimum z coordinate the atoms may get top_z: real number the maximum z coordinate the atoms may get minimum_distance: real number the minimum allowed separation between the added atoms """ if top_z < bottom_z: print "Maximum z must be greater than minimum z. Swapping the values." (top_z, bottom_z) = (bottom_z, top_z) imps = 0 tries = 0 positions = [] minimum_squared = minimum_distance*minimum_distance while imps < number: if tries >= 1000: print "Cannot find space for new atoms, terminating." break rx = nr.random() * self.cell_width ry = nr.random() * self.cell_width rz = nr.random() * (top_z-bottom_z) + bottom_z new_position = [rx, ry, rz] if imps == 0: # add the first atom positions = np.array([new_position]) imps += 1 else: # check that the new position is not too close to previous ones for old_pos in positions: separation = np.array([0.0, 0.0, 0.0]) # take periodic boundary conditions in to account for i in range(3): separation[i] = new_position[i]-old_pos[i] if separation[i] > self.cell_width/2.0: separation[i] - self.cell_width elif separation[i] < self.cell_width/2.0: separation[i] + self.cell_width distance_squared = np.dot(separation, separation) if distance_squared < minimum_squared: tries += 1 break else: # if all the previous positions were far away positions = np.append(positions, [new_position], axis=0) imps += 1 tries = 0 print "Creating {ni} atoms.".format(ni=str(imps)) atoms = Atoms(element+str(imps), positions) self.system += atoms self.system.set_cell([ [self.cell_width, 0.0, 0.0], [0.0, self.cell_width, 0.0], [0.0, 0.0, 100.0] ]) self.system.set_pbc([True, True, False])
[docs] def create_atoms(self,element,positions): """Creates atoms of the given element in the given positions. The atoms are stored in :attr:`tools.FrictionSimulation.pieces`. Once you've added all the pieces you need, finalize the system with :meth:`tools.FrictionSimulation.finalize_system`. Parameters: element: string the chemical symbol of the atoms, e.g., 'C' positions: array of real numbers the xyz coordinates of the atoms, in an array, e.g., ``[[0, 0, 0], [1, 1, 1]]`` """ atoms = Atoms(element+str(len(positions)), np.array(positions)) self.system += atoms
[docs] def create_interaction(self, element_pair, strength, equilibrium_distance, type='LJ'): """Creates an interaction between the two given types of atoms. The interactions are described by the simple Lennard-Jones potential (http://en.wikipedia.org/wiki/Lennard-Jones_potential) .. math:: V(r) = \\varepsilon \\left[ \\left( \\frac{\\sigma}{r} \\right)^12 - \\left( \\frac{\\sigma}{r} \\right)^6 \\right] This is not a realistic interaction except for some very special cases such as noble gases. However, it is simple and will work fine as a placeholder. The method creates an interaction between two types of atoms as defined by ``element_pair``. The types may be the same, so for instance if you have ``'Au'`` atoms in your system, you can make them interact with ``create_interaction( ['Au', 'Au'], ...)`` Parameters: element_pair: string list the types that will interact, e.g, ``['Au', 'Au']`` or ``['B', 'C']`` strength: real number the depth of the potential energy minimum (:math:`-\\min_r V(r)`) for the pair interaction, in eV equilibrium_distance: real number the atomic distance, :math:`r`, at which :math:`V(r)` has its minimum """ epsilon = 4*strength sigma = equilibrium_distance * 0.890898718 potential = pysic.Potential('LJ', symbols=element_pair, cutoff=1.9*sigma, cutoff_margin=0.8*sigma, parameters=[epsilon, sigma]) self.calc.add_potential(potential)
[docs] def attach_with_springs(self, index_list1, index_list2, strength, cutoff=5.0): """Attaches the atoms from one list with the atoms in another list with springs. The two lists of atoms need to have an equal number of atoms, :math:`N`, and the method will create harmonic interactions between the listed atoms such that the atoms are connected in order: for ``index_list1 = [0, 1, 2]``, ``index_list2 = [3, 4, 5]`` (:math:`N=3`), the interactions will be created for pairs 0-3, 1-4, and 2-5. The potential energy of the interaction is .. math:: V(r) = \\frac{1}{2} k r^2, where :math:`r` is the distance between the atoms. That means the equilibrium distance is 0. You can use the method, for instance, to create harmonic potential wells for atoms by attaching them to constrained ghost atoms. Parameters: index_list1: integer list indices of atoms to be connected index_list2: integer list indices of atoms list1 is being connected to strength: real number strength of the attachment, spring constant :math:`k = \\text{strength}/N` cutoff: real number cutoff for the interaction - if the atoms are farther away from each other than this, the potential is ignored. cutoff is restricted to cutoff < cell_width (default 10.0) because otherwise springs would be attached to periodic replicas as well. """ n = len(index_list1) if(len(index_list2) != n): raise Error("The index lists need to have the same number of atoms.") k = strength/n if cutoff >= self.cell_width: raise Error("cutoff must be smaller than cell_width") for atom1, atom2 in zip(index_list1, index_list2): newpot = pysic.Potential('spring',indices=[atom1,atom2], cutoff=cutoff,parameters=[strength, 0.0]) self.calc.add_potential(newpot)
[docs] def continue_from_trajectory(self, filename='simulation.traj', frame=-1): """Read the system geometry from a previously saved trajectory. Parameters: filename: string name of the trajectory file to be read frame: integer the number of the configuration to be read. by default this is -1, meaning the *last* configuration of the trajectory is read. """ traj = PickleTrajectory(filename) self.system = traj[frame] self.system.set_calculator(self.calc) self.remove_constraints()
[docs] def list_atoms(self): """Lists the elements, indices, and positions of all atoms in the system. """ i = 0 for at in self.system: print "index = ",i,", element = ",at.symbol,", position = ",at.position i += 1
[docs] def duplicate_atoms(self, element, indices): """Creates a set of atoms with the same positions as the atoms with the given indices. The new atoms will be of the given element. This function can be used to create ghost atoms to be used with :meth:`~friction_tools.FrictionSimulation.attach_with_springs` or duplicate parts of the system to be moved in the correct place with :meth:`~friction_tools.FrictionSimulation.move_atoms` Parameters: element: string the chemical symbol of the atoms, e.g., 'C' indices: integer list indices of the atoms to be copied """ i = 0 newpos = [] for at in self.system: if i in indices: newpos.append(at.position) i += 1 if len(newpos) > 0: self.create_atoms(element=element,positions=np.array(newpos)) else: print "no atoms to duplicate"
[docs] def remove_atoms(self, indices): """Removes atoms with the given indices. Parameters: indices: integer list indices of the atoms to be removed """ for i in range(len(self.system),-1,-1): if i in indices: del self.system[i]
[docs] def set_velocities(self, velocity, indices): """Gives the atoms initial velocities. You must give the indices of the atoms whose velocities you are defining as a list. If all the atoms should receive the same velocity, ``velocity`` can be a single vector. You can also, instead, give a list of velocities to define the velocities one by one. In that case, the length of the velocity and index lists must match. Parameters: velocity: real vector or a list of vectors the velocity or velocities of th atoms indices: integer list the indices of the atoms affected """ if len(velocity) == 3: velocity = np.array(len(indices)*[velocity]) if len(velocity) != len(indices): print "the velocity and index lists have different lengths in 'set_velocities'" index = 0 for i in indices: self.system[i].momentum = np.array(velocity[index])*self.system[i].mass index += 1
[docs] def set_temperature(self, temperature=50): """Gives the atoms initial velocities according to the `Maxwell-Boltzmann distribution <http://en.wikipedia.org/wiki/Maxwell–Boltzmann_distribution>`_. The default temperature is 50 K. (Room temperature is about 300 K.) Parameters: temperature: float the initial temperature in K - 50 K by default """ if self.system is None: print "You need to 'finalize_system' before setting the temperature." return MaxwellBoltzmannDistribution(self.system, temperature*units.kB) print ("Set initial velocities according to %6.1f K." % temperature)
[docs] def create_dynamics(self, dt=timestep, temperature=None, coupled_indices=None, strength=0.01): """Defines the type of molecular dynamics to be run. By default, energy conserving dynamics are run, i.e, the microcanonical ensemble is used. That is, the normal `Velocity Verlet <http://en.wikipedia.org/wiki/Velocity_Verlet#Velocity_Verlet>`_ algorithm is applied. NVT dynamics are run if the argument ``temperature`` is given. (NVT: number of particles, volume, and temperature are kept constant - temperature only approximately.) This is done using `Langevin dynamics <http://en.wikipedia.org/wiki/Langevin_dynamics>`_. This Langevin thermostat adds a fictious friction force and a random noise force on all atoms. This may lead to computational artifacts if there is collective motion. It's especially important not to apply the thermostat on atoms which are being dragged or move at a fixed velocity, as the thermostat will add artificial friction on them. In order to select the atoms which are being affected by the thermostat, you can specify their indices as a list with the ``coupled_indices`` argument. - **Note** The atomic structure cannot be altered after this method is called (if temperature is set). Create all the atoms before calling this. Parameters: dt: real number timestep :math:`\\Delta t` used in integrating the equations of motion, in fs - 2 fs by default. temperature: real number temperature in K, if you wish to apply a thermostat coupled_indices: integer list indices of the atoms the thermostat affects strength: real number strength of the thermostat, larger number meaning a stronger deviation from Newtonian mechanics - 0.01 by default """ if not temperature is None: temp = temperature if coupled_indices is None: lan = strength else: lan = [] for at in range(len(self.system)): if at in coupled_indices: lan.append(strength) else: lan.append(0.0) lan = np.array(lan) self.dynamics = LangevinMod(self.system, dt*units.fs, temp*units.kB, lan, fixcm=False) if pysic.get_cpu_id() == 0: print ("Set up Langevin dynamics") else: # Create constant energy dynamics self.dynamics = VelocityVerlet(self.system, dt*units.fs) if pysic.get_cpu_id() == 0: print ("Set up Verlet dynamics") self.timestep = dt
[docs] def calculate_potential_energy(self): """Returns the potential energy of the system. """ return self.system.get_potential_energy()
[docs] def run_simulation(self, time=10, steps=None): """Runs molecular dynamics (MD). This method executes the actual simulation according to the parameters defined earlier by other methods. The simulation may take a long time to run, so it is a very good idea to first run a short test run to get an idea on the execution time. You can time the simulation with:: >>> from tools import * >>> s = FrictionSimulation() >>> # define the system... >>> import time >>> t0 = time.time() >>> s.run_simulation(steps=5) >>> t1 = time.time() >>> print "The simulation took "+str(t1-t0)+" s." The simulation runs for a given length of time, which can be defined as either simulated time or number of molecular dynamics steps. If the argument ``steps`` is given, the simulation runs for this many steps. If ``time`` is given, the simulation is run for this long (in fs). Even if time :math:`t` is specified, the simulation length is still determined in steps :math:`n_\\mathrm{steps} = t/\\Delta t`, where :math:`\\Delta t` is the timestep. Parameters: time: float simulation time in fs, 10 fs by default steps: integer simulation time in MD steps """ if isinstance(self.dynamics,BFGSLineSearch): self.dynamics.run(fmax=0.05) return if steps is None: run_steps = int(time/self.timestep) else: run_steps = steps print "Starting a dynamic simulation." self.dynamics.run(run_steps)
[docs] def remove_constraints(self): """Removes any constraints previously applied on the system. """ self.system.constraints = []
[docs] def fix_positions(self, indices, xyz=[True,True,True]): """Constrains the positions of particles. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` This method freezes coordinates of the atoms whose indices are given. By default, the atoms are not allowed to move at all, but the optional argument ``xyz`` allows one to freeze only some coordinates. For example:: >>> from friction_tools import * >>> s = FrictionSimulation() >>> # define the system... >>> s.fix_positions(indices=[...], xyz=[True,True,False]) freezes the x and y coordinates of the specified atoms (``xyz = [True,True,False]``). Parameters: indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0 xyz: list of three booleans logical tags specifying if the xyz coordinates are fixed (``True`` means the coordinate is fixed) """ c = FixPositions(indices, xyz) self.system.constraints.append(c)
[docs] def fix_velocities(self, indices, velocity, xyz=[True,True,True]): """Constrains the velocities of particles. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` This method freezes velocities of the atoms whose indices are given. By default, the atoms move with constant velocity, but the optional argument ``xyz`` allows one to fix only a given component of force. For example:: >>> from tools import * >>> s = FrictionSimulation() >>> # define the system... >>> s.fix_velocities(indices=[...], velocity=[0.01,0.0,0.0], xyz=[True,False,True]) fixes the velocities in x and z directions to :math:`v_x = 0.01, v_z = 0.0` Ã…/fs (``xyz = [True,False,True]``) but allows the velocity to change freely in the y direction. Parameters: indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0 velocity: list of three floats the velocity given to the particles xyz: list of three booleans logical tags specifying if the xyz coordinates are fixed (``True`` means the coordinate is fixed) """ c = FixVelocities(indices, velocity, self.timestep, xyz) self.system.constraints.append(c)
[docs] def fix_positions_on_line(self, indices, direction): """Constrains the positions of particles on a line. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` This method constrains coordinates of the atoms whose indices are given on a line. The line is defined by the given direction :math:`\\mathbf{u}` and the initial position of the particle :math:`\\mathbf{r}_0`. .. math:: \\mathbf{r} - \\mathbf{r}_0 \\parallel \\mathbf{u} Parameters: indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0 direction: list of three floats the direction ``[ux, uy, uz]`` in which the particles are allowed to move """ c = FixLine(indices, direction) self.system.constraints.append(c)
[docs] def fix_positions_on_plane(self, indices, normal): """Constrains the positions of particles on a plane. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` This method constrains coordinates of the atoms whose indices are given on a plane. The plane is defined by the given normal :math:`\\mathbf{n}` and the initial position of the particle :math:`\\mathbf{r}_0`. .. math:: \\mathbf{r} - \\mathbf{r}_0 \\perp \\mathbf{n} Parameters: indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0 normal: list of three floats the normal vector ``[nx, ny, nz]`` to the plane in which the particles are allowed to move """ c = FixPlane(indices, normal) self.system.constraints.append(c)
[docs] def add_constant_force(self, indices, force): """Adds a constant external force on atoms. Adds an external force on the specified atoms .. math:: \\mathbf{F} = f_x \\mathbf{i} + f_y \\mathbf{j} + f_z \\mathbf{k}. Parameters: indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0 force: list of three floats the force ``[fx, fy, fz]`` to be applied on the given atoms """ index_list = [] for index in indices: index_list.append([index]) pot = pysic.Potential('force', indices=index_list, parameters=force) self.calc.add_potential(pot)
[docs] def print_stats(self, time_it=False): """Prints the energy and temperature information in human readable format. Parameters: time_it: boolean if ``True``, the calculation is timed and the execution time is printed """ t0 = time.time() epot = self.system.get_potential_energy() / len(self.system) t1 = time.time() ekin = self.system.get_kinetic_energy() / len(self.system) t2 = time.time() stress = self.system.get_stress() t3 = time.time() step = self._get_step() if pysic.get_cpu_id() == 0: if step > 0: print ("Timestep: %7i" % step) print ("Energy per atom: Epot = %.3f eV, Ekin = %.3f eV, Etot = %.3f eV" % (epot, ekin, epot+ekin)) print ("Temperature = %.3f K\n" % (ekin/(1.5*units.kB))) if time_it: print ("Energy calculation took %.3f s, force and stress calculation took %.3f s\n" % (t1-t0, t3-t2))
[docs] def write_positions_to_file(self, filename='positions.txt', indices=None, addStepNumber=False): """Writes the coordinates of the atoms in a file. The format is:: (atom 0 x) (atom 0 y) (atom 0 z) (atom 1 x) (atom 1 y) (atom 1 z) (atom 2 x) (atom 2 y) (atom 2 z) ... Parameters: filename: string name of the file where the information is written indices: list of integers the indices of the atoms whose info is to be written - note that Python indexing starts from 0. By default all atoms are included addStepNumber: boolean meant for internal use - if ``True``, adds the MD step number in the filename to prevent overwriting """ if indices is None: indices = range(len(self.system)) output_lines = "" for index in indices: [x,y,z] = self.system[index].get_position() output_lines += (" %20.5f %20.5f %20.5f \n" % (x,y,z) ) filing = add_number_to_filename(filename, self._get_step(), addStepNumber) write_file(filing, output_lines)
[docs] def write_velocities_to_file(self, filename='velocities.txt', indices=None, addStepNumber=False): """Writes the instantaneous velocities of the atoms in a file. - **Note** Due to a bug/feature in ASE, if you fix the velocities of atoms with :meth:`tools.FrictionSimulation.fix_velocities`, the velocities for these atoms are reported to be 0.0. The method :meth:`tools.FrictionSimulation.gather_average_velocities_during_simulation` does report the correct average velocities for all atoms though. The format is:: (atom 0 v_x) (atom 0 v_y) (atom 0 v_z) (atom 1 v_x) (atom 1 v_y) (atom 1 v_z) (atom 2 v_x) (atom 2 v_y) (atom 2 v_z) ... Parameters: filename: string name of the file where the information is written indices: list of integers the indices of the atoms whose info is to be written - note that Python indexing starts from 0. By default all atoms are included addStepNumber: boolean meant for internal use - if ``True``, adds the MD step number in the filename to prevent overwriting """ if indices is None: indices = range(len(self.system)) output_lines = "" for index in indices: try: [x,y,z] = self.system[index].get_momentum()/self.system[index].get_mass() except: [x,y,z] = [0.0,0.0,0.0] output_lines += (" %20.5f %20.5f %20.5f \n" % (x,y,z) ) filing = add_number_to_filename(filename, self._get_step(), addStepNumber) write_file(filing, output_lines)
[docs] def write_forces_to_file(self, filename='forces.txt', indices=None, addStepNumber=False): """Writes the instantaneous forces of the atoms in a file. The format is:: (atom 0 f_x) (atom 0 f_y) (atom 0 f_z) (atom 1 f_x) (atom 1 f_y) (atom 1 f_z) (atom 2 f_x) (atom 2 f_y) (atom 2 f_z) ... Parameters: filename: string name of the file where the information is written indices: list of integers the indices of the atoms whose info is to be written - note that Python indexing starts from 0. By default all atoms are included addStepNumber: boolean meant for internal use - if ``True``, adds the MD step number in the filename to prevent overwriting """ if indices is None: indices = range(len(self.system)) output_lines = "" forces = self.system.get_forces(apply_constraint=False) for index in indices: [x,y,z] = forces[index] output_lines += (" %20.5f %20.5f %20.5f \n" % (x,y,z) ) filing = add_number_to_filename(filename, self._get_step(), addStepNumber) write_file(filing, output_lines)
[docs] def write_average_force_to_file(self, filename='force_sum.txt', indices=None): """Writes the average of instantaneous forces of the atoms in a file. The format is:: (f_x) (f_y) (f_z) ... Parameters: filename: string name of the file where the information is written indices: list of integers the indices of the atoms whose info is to be written - note that Python indexing starts from 0. By default all atoms are included """ if indices is None: indices = range(len(self.system)) output_lines = "" forces = self.system.get_forces(apply_constraint=False) force_sum = np.array([0.0,0.0,0.0]) for index in indices: force_sum += forces[index] [x,y,z] = force_sum / float(len(indices)) output_line = (" %20.5f %20.5f %20.5f \n" % (x,y,z) ) append_file(filename, output_line)
[docs] def write_average_position_to_file(self, filename='avr_position.txt', indices=None): """Writes the mean of the coordinates of the atoms in a file. The format is:: (x) (y) (z) Parameters: filename: string name of the file where the information is written indices: list of integers the indices of the atoms whose info is to be written - note that Python indexing starts from 0. By default all atoms are included """ if indices is None: indices = range(len(self.system)) output_lines = "" pos = self.system.get_positions() pos_sum = np.array([0.0,0.0,0.0]) for index in indices: pos_sum += pos[index] [x,y,z] = pos_sum / float(len(indices)) output_line = (" %20.5f %20.5f %20.5f \n" % (x,y,z) ) append_file(filename, output_line)
[docs] def write_energy_and_temperature_to_file(self, filename='energies.txt'): """Appends the instantaneous energy and temperature information in a file. The format is:: (potential energy) (kinetic energy) (total energy) (temperature) Since this method does not write atom-by-atom information, it does not create a new file but appends its output at the end of the specified file. Parameters: filename: string name of the file where the information is written """ e_pot = self.system.get_potential_energy() e_kin = self.system.get_kinetic_energy() t = e_kin/(len(self.system)*(1.5*units.kB)) output_lines = (" %20.5f %20.5f %20.5f %20.5f \n" % (e_pot, e_kin, (e_pot+e_kin), t) ) append_file(filename, output_lines)
def _record_positions(self): """For internal use. Saves the positions of the atoms.""" self.old_positions = self.system.get_positions() def _record_work(self): """For internal use. Integrates work done on all atoms.""" forces = self.system.get_forces(apply_constraint=False) move = self.system.get_positions() move -= self.prev_positions for index in range(len(self.system)): self.work[index] += np.dot((forces[index]+self.prev_forces[index])*0.5, move[index]) self.prev_positions = self.system.get_positions() self.prev_forces = forces def _clear_work(self): """For internal use. Clears the recorded work integrals.""" self.work = np.zeros(len(self.system),dtype=float) self.prev_positions = self.system.get_positions() self.prev_forces = self.system.get_forces(apply_constraint=False)
[docs] def write_work_to_file(self, filename='work.txt', indices=None, addStepNumber=False): """For internal use. Writes the work done on the atoms in a file. The format is:: (atom 0 work) (atom 1 work) (atom 2 work) ... Parameters: filename: string name of the file where the information is written indices: list of integers the indices of the atoms whose info is to be written - note that Python indexing starts from 0. By default all atoms are included addStepNumber: boolean meant for internal use - if ``True``, adds the MD step number in the filename to prevent overwriting """ if indices is None: indices = range(len(self.system)) output_lines = "" for index in indices: output_lines += (" %20.5f \n" % (self.work[index]) ) #self._clear_work() filing = add_number_to_filename(filename, self._get_step(), addStepNumber) write_file(filing, output_lines)
[docs] def write_total_work_to_file(self,filename='total_work.txt', indices=None): """For internal use. Writes the work done on the atoms in a file. Parameters: filename: string name of the file where the information is written indices: list of integers the indices of the atoms whose info is to be written - note that Python indexing starts from 0. By default all atoms are included """ if indices is None: indices = range(len(self.system)) output_lines = "" work_sum = 0.0 for index in indices: work_sum += self.work[index] output_line = (" %20.5f \n" % (work_sum) ) append_file(filename, output_line)
[docs] def write_average_velocities_to_file(self, dt, filename='avr_velocities.txt', indices=None, addStepNumber=False): """For internal use. Writes the average velocities of the atoms in a file. The format is:: (atom 0 v_x) (atom 0 v_y) (atom 0 v_z) (atom 1 v_x) (atom 1 v_y) (atom 1 v_z) (atom 2 v_x) (atom 2 v_y) (atom 2 v_z) ... Parameters: filename: string name of the file where the information is written indices: list of integers the indices of the atoms whose info is to be written - note that Python indexing starts from 0. By default all atoms are included addStepNumber: boolean meant for internal use - if ``True``, adds the MD step number in the filename to prevent overwriting """ if indices is None: indices = range(len(self.system)) output_lines = "" for index in indices: pos = np.array(self.system[index].get_position()) pos -= self.old_positions[index] pos /= dt*units.fs [x,y,z] = pos output_lines += (" %20.5f %20.5f %20.5f \n" % (x,y,z) ) self._record_positions() filing = add_number_to_filename(filename, self._get_step(), addStepNumber) append_file(filing, output_lines)
[docs] def print_stats_during_simulation(self, interval=10): """Makes the simulator print energies and temperature during simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` This method instructs the simulator to call :meth:`tools.FrictionSimulation.print_stats` periodically during the molecular dynamics run, every ``interval`` steps. This is a convenient way to monitor the progress of your simulation. Parameters: interval: integer the number of MD steps between consecutive information writing """ self.dynamics.attach(self.print_stats, interval)
[docs] def gather_positions_during_simulation(self, interval=10, filename='positions.txt', indices=None): """Makes the simulator write coordinates of atoms during simulation. The format is:: (atom 0 x) (atom 0 y) (atom 0 z) (atom 1 x) (atom 1 y) (atom 1 z) (atom 2 x) (atom 2 y) (atom 2 z) ... - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` This method instructs the simulator to call :meth:`tools.FrictionSimulation.write_positions_to_file` periodically during the molecular dynamics run, every ``interval`` steps. The given filename is automatically modified to include the current MD step number. For instance, if the filename is given as ``data.txt``, the data from, say, step 20 will be written to file ``data00020.txt``. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0. By default, all atoms are included. """ self.dynamics.attach(self.write_positions_to_file, interval, filename, indices, True)
[docs] def gather_velocities_during_simulation(self, interval=10, filename='velocities.txt', indices=None): """Makes the simulator write instantaneous velocities of atoms during simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` - **Note** Due to a bug/feature in ASE, if you fix the velocities of atoms with :meth:`tools.FrictionSimulation.fix_velocities`, the velocities for these atoms are reported to be 0.0. The method :meth:`tools.FrictionSimulation.gather_average_velocities_during_simulation` does report the correct average velocities for all atoms though. The format is:: (atom 0 v_x) (atom 0 v_y) (atom 0 v_z) (atom 1 v_x) (atom 1 v_y) (atom 1 v_z) (atom 2 v_x) (atom 2 v_y) (atom 2 v_z) ... This method instructs the simulator to call :meth:`tools.FrictionSimulation.write_velocities_to_file` periodically during the molecular dynamics run, every ``interval`` steps. The given filename is automatically modified to include the current MD step number. For instance, if the filename is given as ``data.txt``, the data from, say, step 20 will be written to file ``data00020.txt``. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0. By default, all atoms are included. """ self.dynamics.attach(self.write_velocities_to_file, interval, filename, indices, True)
[docs] def gather_average_velocities_during_simulation(self, interval=10, filename='avr_velocities.txt', indices=None): """Makes the simulator write average velocities of atoms during simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` The format is:: (atom 0 v_x) (atom 0 v_y) (atom 0 v_z) (atom 1 v_x) (atom 1 v_y) (atom 1 v_z) (atom 2 v_x) (atom 2 v_y) (atom 2 v_z) ... This method instructs the simulator to call :meth:`tools.FrictionSimulation.write_average_velocities_to_file` periodically during the molecular dynamics run, every ``interval`` steps. The average velocities recorded are the averages since the previous recording. So, when writing at intervals of :math:`n_\\mathrm{avr}`, the average velocity of atom :math:`i` at time :math:`t` will be the atomic displacement :math:`\\Delta \\mathbf{r}_i(t) = \\mathbf{r}_i(t) - \\mathbf{r}_{i}(t-\\Delta t)` divided by the elapsed time :math:`\\Delta t = n_\\mathrm{avr} \\Delta t'`, with :math:`\\Delta t'` being the MD timestep: .. math:: \\mathbf{v}_{i,\\mathrm{avr}}(t-\\Delta t, t) = \\frac{\\Delta \\mathrm{r}_i}{\\Delta t_i}. The given filename is automatically modified to include the current MD step number. For instance, if the filename is given as ``data.txt``, the data from, say, step 20 will be written to file ``data00020.txt``. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0. By default, all atoms are included. """ self._record_positions() self.dynamics.attach(self.write_average_velocities_to_file, interval, interval*self.timestep, filename, indices, True)
[docs] def gather_forces_during_simulation(self, interval=100, filename='forces.txt', indices=None): """Makes the simulator write instantaneous forces of atoms during simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` The format is:: (atom 0 f_x) (atom 0 f_y) (atom 0 f_z) (atom 1 f_x) (atom 1 f_y) (atom 1 f_z) (atom 2 f_x) (atom 2 f_y) (atom 2 f_z) ... This method instructs the simulator to call :meth:`tools.FrictionSimulation.write_forces_to_file` periodically during the molecular dynamics run, every ``interval`` steps. The given filename is automatically modified to include the current MD step number. For instance, if the filename is given as ``data.txt``, the data from, say, step 20 will be written to file ``data00020.txt``. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0. By default, all atoms are included. """ self.dynamics.attach(self.write_forces_to_file, interval, filename, indices, True)
[docs] def gather_average_position_during_simulation(self, interval=100, filename='avr_position.txt', indices=None): """Makes the simulator write averages of positions of atoms during simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` The format is:: (x) (y) (z) This method instructs the simulator to call :meth:`tools.FrictionSimulation.write_average_position_to_file` periodically during the molecular dynamics run, every ``interval`` steps. The data is written in a single file where each line represents a point in the time series. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0. By default, all atoms are included. """ write_file(filename, "") # clear the file self.dynamics.attach(self.write_average_position_to_file, interval, filename, indices)
[docs] def gather_average_force_during_simulation(self, interval=100, filename='avr_force.txt', indices=None): """Makes the simulator write averages of instantaneous forces of atoms during simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` The format is:: (f_x) (f_y) (f_z) This method instructs the simulator to call :meth:`tools.FrictionSimulation.write_average_force_to_file` periodically during the molecular dynamics run, every ``interval`` steps. The data is written in a single file where each line represents a point in the time series. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0. By default, all atoms are included. """ write_file(filename, "") # clear the file self.dynamics.attach(self.write_average_force_to_file, interval, filename, indices)
[docs] def gather_total_work_during_simulation(self, interval=100, filename='total_work.txt', indices=None): """Makes the simulator write the total work done on atoms during simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` This method instructs the simulator to call :meth:`tools.FrictionSimulation.write_total_work_to_file` periodically during the molecular dynamics run, every ``interval`` steps. The data is written in a single file where each line represents a point in the time series. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0. By default, all atoms are included. """ write_file(filename, "") # clear the file self._clear_work() self.dynamics.attach(self._record_work, interval=1) self.dynamics.attach(self.write_total_work_to_file, interval, filename, indices)
[docs] def gather_energy_and_temperature_during_simulation(self, interval=100, filename='energy.txt'): """Makes the simulator write energy and temperature during simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` The format is:: (potential energy) (kinetic energy) (total energy) (temperature) This method instructs the simulator to call :meth:`tools.FrictionSimulation.write_energy_and_temperature_to_file` periodically during the molecular dynamics run, every ``interval`` steps. The data is written in a single file where each line represents a point in the time series. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written """ write_file(filename, "") # clear the file self.dynamics.attach(self.write_energy_and_temperature_to_file, interval, filename)
[docs] def gather_work_during_simulation(self, interval=100, filename='work.txt', indices=None): """Makes the simulator write work done on atoms during simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` The format is:: (atom 0 work) (atom 1 work) (atom 2 work) ... This method instructs the simulator to call :meth:`tools.FrictionSimulation.write_work_to_file` periodically during the molecular dynamics run, every ``interval`` steps. Work is defined as the line integral of the force. The recorded values are the work done on each individual atom since the previous recording. So, when writing at intervals of :math:`n_\\mathrm{avr}`, the work on atom :math:`i` at time :math:`t` is .. math:: W_i(t-\\Delta t, t) = \\int_{t-\\Delta t}^t \\mathbf{F}_i \\cdot \\mathrm{d}\\mathbf{r}_i(t) with :math:`\\Delta t = n_\\mathrm{avr} \\Delta t'`, :math:`\\Delta t'` being the MD timestep. The given filename is automatically modified to include the current MD step number. For instance, if the filename is given as ``data.txt``, the data from, say, step 20 will be written to file ``data00020.txt``. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written indices: list of integers the indices of the atoms to be constrained - note that Python indexing starts from 0. By default, all atoms are included. """ self._clear_work() self.dynamics.attach(self._record_work, interval=1) self.dynamics.attach(self.write_work_to_file, interval, filename, indices, True)
[docs] def save_trajectory_during_simulation(self, interval=100, filename='simulation.traj'): """Saves the simulation trajectory in binary format. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` This method saves the simulation as a trajectory file, from which the positions of particles can be extracted at the different points of time of the simulation. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written """ if not self.trajectory is None: #self.trajectory.close() mode = 'a' else: mode = 'w' self.trajectory = PickleTrajectory(filename, mode, self.system, master=( pysic.get_cpu_id() == 0 )) self.dynamics.attach(self.trajectory.write, interval)
[docs] def take_snapshots_during_simulation(self, interval=100, filename='snapshot.png'): """Takes png-format snapshots of the system during the simulation. - **Note** call this method before running the simulation with :meth:`tools.FrictionSimulation.run_simulation` This method generates images of the system in png format. Parameters: interval: integer the number of MD steps between consecutive information writing filename: string name of the file where the data is written """ self.dynamics.attach(self.take_snapshot, interval, filename, True)
[docs] def load_state_from_trajectory(self, step=-1, filename='simulation.traj'): """Loads the geometry from a saved trajectory. If you have a saved trajectory from :meth:`tools.FrictionSimulation.save_trajectory_during_simulation`, this method can be used for reading in a state from the trajectory. Parameters: step: integer the timestep which is restored - by default the last recorded state is chosen filename: string name of the trajectory file """ traj_in = PickleTrajectory(filename) self.system = traj_in[step]
def _get_step(self): """Returns the number of steps the dynamics has run. """ try: return self.dynamics.get_number_of_steps() except: return 0
[docs] def take_snapshot(self, filename='snapshot.png', addStepNumber=False): """Makes a png format snapshot of the system. Parameters: filename: string name of the trajectory file addStepNumber: boolean meant for internal use - if ``True``, adds the MD step number in the filename to prevent overwriting """ filing = add_number_to_filename(filename, self._get_step(), addStepNumber) ase.io.write(filing, self.system, format='png', rotation='-80x')
[docs] def write_xyzfile(self, filename='system.xyz'): """Makes an xyz format geometry file of the system. XYZ is a commonly used, very simple geometry file format which includes information on the types of atoms and their coordinates. This method writes such a file based on the current simulation geometry. XYZ files can be read with almost any atomic visualization tool. Parameters: filename: string name of the trajectory file """ ase.io.write(filename, self.system, format='xyz')
[docs] def get_indices_z_less_than(self, z_limit): """Returns the indices of the atoms whose z-coordinate is less than the given value, as a list. This can be used for easily finding, e.g., the bottom of a slab. Parameters: z_limit: float the limiting z value """ return [atom.index for atom in self.system if atom.z < z_limit]
[docs] def get_indices_z_more_than(self, z_limit): """Returns the indices of the atoms whose z-coordinate is more than the given value, as a list. This can be used for easily finding, e.g., the top of a slab. Parameters: z_limit: float the limiting z value """ return [atom.index for atom in self.system if atom.z > z_limit]
[docs] def get_indices_by_element(self, element): """Returns the indices of atoms of given element. Parameters: element: string the chemical symbol of the atoms, e.g., 'C' """ return [atom.index for atom in self.system if atom.symbol == element]
[docs]def read_arrays_from_files(filename, starting_index, ending_index, index_step): """Reads numeric arrays from files. If you have run a simulation and produced files containing numeric data through the methods ``gather_xxx_during_simulation``, this function can be used for reading the data in Python. You should have files with names such as ``positions00100.txt`` etc. To read them in, give the base name ``positions.txt`` as the ``filename``, and the range of integers through the other arguments: ``starting_index`` and ``ending_index`` are the first and last index to be read, respectively, while ``index_step`` is the increment. Example:: >>> read_arrays_from_files('data.txt',10,100,10) will read the files:: data00010.txt data00020.txt ... data00100.txt The function returns a list containing all the arrays extracted from the files. Parameters: filename: string the base filename to be read starting_index: integer the first index to be read ending_index: integer the last index to be read index_step: integer the increment in indices """ array_collection = [] for index in range(starting_index, ending_index+index_step, index_step): filing = add_number_to_filename(filename, index, True) new_array = read_array_from_file(filing) array_collection.append(new_array) return array_collection
[docs]def read_array_from_file(filename): """Parses a file and returns a numeric array of the data found. Parameters: filename: string name of the file to be read """ array = np.fromfile(filename, dtype=float, count=-1, sep=' ') f=file(filename) lines = f.readlines() f.close() array.shape = (len(lines), array.size/len(lines)) return array
[docs]def write_array_to_file(filename, array): """Writes an array to a file. **Note** The method calls the Numpy routine:: array.tofile(filename, sep=" ", format="%20.5f") which results in all the values stored in the array being written in one line. If you read it back in with :meth:`tools.read_array_from_file`, you get a 1D array containing the values. To fix the shape, specify it explicitly:: >>> arry = [[1, 2], [3, 4]] array([[1, 2], [3, 4]]) >>> write_array_to_file(filename='array.txt', arry) >>> new_arry = read_array_from_file('array.txt') >>> new_arry array([[1, 2, 3, 4]]) >>> new_arry.shape = (2,2) >>> new_arry array([[1, 2], [3, 4]]) Parameters: filename: string name of the file to be written array: numeric array the array to be written """ array.tofile(filename, sep=" ", format="%20.5f")
[docs]def add_arrays_by_component(array1, array2): """Adds two arrays component by component. Equal to ``np.array(array1) + np.array(array2)`` Parameters: array1: numeric array of floats an array array2: numeric array of floats an array """ return np.array(array1) + np.array(array2)
[docs]def subtract_arrays_by_component(array1, array2): """Subtracts two arrays component by component. Equal to ``np.array(array1) - np.array(array2)`` Parameters: array1: numeric array of floats an array array2: numeric array of floats an array """ return np.array(array1) - np.array(array2)
[docs]def multiply_arrays_by_component(array1, array2): """Multiplies two arrays component by component. Equal to ``np.array(array1) * np.array(array2)`` Parameters: array1: numeric array of floats an array array2: numeric array of floats an array """ return np.array(array1) * np.array(array2)
[docs]def divide_arrays_by_component(array1, array2): """Divides two arrays component by component. Equal to ``np.array(array1) / np.array(array2)`` Parameters: array1: numeric array of floats an array array2: numeric array of floats an array """ return np.array(array1) / np.array(array2)
[docs]def add_to_array(array, real): """Adds a real number to an array component by component. Equal to ``np.array(array) + real`` Parameters: array: numeric array of floats an array real: float a number """ return np.array(array) + real
[docs]def multiply_array(array, real): """Multiplies an array with a real number component by component. Equal to ``np.array(array) * real`` Parameters: array: numeric array of floats an array real: float a number """ return np.array(array1) * real
[docs]def create_array(dimensions, value=0): """Creates an array of the given size. Parameters: dimensions: list of integers dimensions of the array value: float the initial value for the elements of the array """ return np.zeros(dimensions) + value
[docs]def concatenate_arrays_by_rows(array1, array2): """Joins two arrays together by appending row by row. Parameters: array1: numeric array of floats an array array2: numeric array of floats an array """ cols = array1.shape[1] rows1 = array1.shape[0] rows2 = array2.shape[0] newarray = np.zeros((rows1+rows2,cols)) for i in range(rows1): newarray[i,:] = array1[i,:] for i in range(rows2): newarray[i+rows1,:] = array2[i,:] return newarray
[docs]def concatenate_arrays_by_columns(array1, array2): """Joins two arrays together by appending column by column. Parameters: array1: numeric array of floats an array array2: numeric array of floats an array """ cols1 = array1.shape[1] rows = array1.shape[0] cols2 = array2.shape[1] newarray = np.zeros((rows,cols1+cols2)) for i in range(cols1): newarray[:,i] = array1[:,i] for i in range(cols2): newarray[:,i+cols1] = array2[:,i] return newarray
[docs]def shift_array_by_rows(array, steps=1, wrap=False): """Moves the elements in an array by rows. Parameters: array: numeric array of floats an array steps: integer number of rows the elements are moved wrap: boolean if ``True``, elements that go over the bounds of the array appear on the other side """ newarray = np.zeros_like(array) newarray[steps:] = np.array(array)[:-steps] if wrap: newarray[:steps] = np.array(array)[-steps:] return newarray
[docs]def shift_array_by_columns(array, steps=1, wrap=False): """Moves the elements in an array in by columns. Parameters: array: numeric array of floats an array steps: integer number of rows the elements are moved wrap: boolean if ``True``, elements that go over the bounds of the array appear on the other side """ newarray = np.zeros_like(array) newarray[:,steps:] = np.array(array)[:,:-steps] if wrap: newarray[:,:steps] = np.array(array)[:,-steps:] return newarray
[docs]def crop_array(top, left, bottom, right, array): """Crops an array (i.e., returns a subarray). The indices for cropping should be given as arguments. The order is ``top, left, bottom, right``. For ``top`` and ``left``, they are the first indices to be included, while ``bottom`` and ``right`` are the first indices to be dropped. For instance, if you have the array:: arry = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] The top left :math:`2 \\times 2` array is obtained with:: >>> crop_array(0, 0, 2, 2, arry) [[1, 2], [4, 5]] The command is equivalent to:: >>> array.copy()[top:bottom,left:right] Parameters: top: integer first index from the top to be included left: integer first integer from the left to be included bottom: integer first index from the top to be left out rigth: integer first index from the left to be left out """ return array.copy()[top:bottom,left:right]
[docs]def calculate_row_sums(array): """Sums all the rows of a 2D array and returns a row 1D array containing the results. Parameters: array: numeric array the array to be manipulated """ if len(array.shape) < 3: height = array.shape[0] result = np.zeros(height) for i in range(height): row = array[i,:] row_sum = np.sum(row) result[i] = row_sum else: raise Exception("Row sum is only defined for 1- or 2-dimensional arrays") return result
[docs]def calculate_row_norms(array): """Calculates euclidian norms for all the rows of a 2D array and returns a row 1D array containing the results. Parameters: array: numeric array the array to be manipulated """ if len(array.shape) < 3: height = array.shape[0] result = np.zeros(height) for i in range(height): row = array[i,:] row_sum = sqrt(np.dot(row,row)) result[i] = row_sum else: raise Exception("Row norm is only defined for 1- or 2-dimensional arrays") return result
[docs]def calculate_row_maxima(array): """Finds the maxima for all the rows of a 2D array and returns a row 1D array containing the results. Parameters: array: numeric array the array to be manipulated """ if len(array.shape) < 3: height = array.shape[0] result = np.zeros(height) for i in range(height): row = array[i,:] row_sum = np.max(row) result[i] = row_sum else: raise Exception("Row max is only defined for 1- or 2-dimensional arrays") return result
[docs]def calculate_row_minima(array): """Finds the minima for all the rows of a 2D array and returns a row 1D array containing the results. Parameters: array: numeric array the array to be manipulated """ if len(array.shape) < 3: height = array.shape[0] result = np.zeros(height) for i in range(height): row = array[i,:] row_sum = np.min(row) result[i] = row_sum else: raise Exception("Row min is only defined for 1- or 2-dimensional arrays") return result
[docs]def calculate_column_sums(array): """Sums all the columns of a 2D array and returns a row 1D array containing the results. Parameters: array: numeric array the array to be manipulated """ if len(array.shape) == 2: width = array.shape[1] result = np.zeros(width) for i in range(width): column = array[:,i] column_sum = np.sum(column) result[i] = column_sum elif len(array.shape) == 1: result = array else: raise Exception("Column sum is only defined for 1- or 2-dimensional arrays") return result
[docs]def calculate_column_norms(array): """Calculates euclidian norms for all the columns of a 2D array and returns a row 1D array containing the results. Parameters: array: numeric array the array to be manipulated """ if len(array.shape) == 2: width = array.shape[1] result = np.zeros(width) for i in range(width): column = array[:,i] column_sum = sqrt(np.dot(column,column)) result[i] = column_sum elif len(array.shape) == 1: result = array else: raise Exception("Column norm is only defined for 1- or 2-dimensional arrays") return result
[docs]def calculate_column_maxima(array): """Finds the maxima for all the columns of a 2D array and returns a row 1D array containing the results. Parameters: array: numeric array the array to be manipulated """ if len(array.shape) == 2: width = array.shape[1] result = np.zeros(width) for i in range(width): column = array[:,i] column_sum = np.max(column) result[i] = column_sum elif len(array.shape) == 1: result = array else: raise Exception("Column max is only defined for 1- or 2-dimensional arrays") return result
[docs]def calculate_column_minima(array): """Finds the minima for all the columns of a 2D array and returns a row 1D array containing the results. Parameters: array: numeric array the array to be manipulated """ if len(array.shape) == 2: width = array.shape[1] result = np.zeros(width) for i in range(width): column = array[:,i] column_sum = np.min(column) result[i] = column_sum elif len(array.shape) == 1: result = array else: raise Exception("Column min is only defined for 1- or 2-dimensional arrays") return result
[docs]def plot_array(x_values, y_values=None, y_error=None, x_range=None, y_range=None): """Plots data. If only one column of data is given, it is plotted against a running index. If two columns of data are given, the first is treated as x and the second as y. **Note** You should also use :func:`tools.hold_plots`. Parameters: x_values: list of floats If it's the only input given, it is treated as the y-variable. If ``y_values`` is given, this is the x-variable. y_values: list of floats Values of the y-variable. y_error: list of floats Values for errorbars of y. x_range: list of two floats Plotting range for x y_range: list of two floats Plotting range for y """ if y_values == None: xs = np.array(range(len(x_values))) ys = np.array(x_values) else: xs = np.array(x_values) ys = np.array(y_values) if not x_range is None: plt.ylim(x_range[0],x_range[1]) if not y_range is None: plt.ylim(y_range[0],y_range[1]) if y_error == None: plt.plot(xs,ys) else: es = np.array(y_error) errorbar(xs, ys, yerr=es, fmt='ro')
[docs]def plot_zero_line(): """Adds a horizontal line at y=0 to the plot. """ plt.axhline()
[docs]def hold_plots(): """Hold the plot visible. By default, your plots will vanish from the screen after plotting. This function prevents it. The point is, you can call :func:`tools.plot_array` several times and the generated plots will be added together and shown when you call this function. """ plt.show()
[docs]def trajectory_to_xyz(filename_in='simulation.traj',filename_out='simulation.xyz'): """Converts a trajectory file to an XYZ file. Parameters: filaname_in: string name of the trajectory file filaname_out: string name of the XYZ file """ # Write the trajectory to an xyz file if pysic.get_cpu_id() == 0: traj = PickleTrajectory(filename_in) movie = [] for snapshot in traj: movie.append(snapshot) ase.io.write(filename_out,movie,format='xyz')
def add_number_to_filename(filename, number, act=True): if act: name = filename.split('.') try: filing = ( (name[0]+"%5.5i."+name[1]) % number ) except: filing = ( (filename+"%5.5i") % number ) else: filing = filename return filing
[docs]def write_file(filename, output_lines): """Writes the given string in a file. Note that you should provide only a single string. If you want to split the output to several lines, use the newline character ``\\n``. If the file exists, it is overwritten. Parameters: filename: string name of the file to be written output_lines: string the string to be written """ if pysic.get_cpu_id() == 0: # if we run MPI, only write with the master CPU f = file(filename,'w') f.write(output_lines) f.close()
[docs]def append_file(filename, output_lines): """Writes the given string in a file. Note that you should provide only a single string. If you want to split the output to several lines, use the newline character '\\n'. This function does not overwrite a file, but appends the output at the end. Parameters: filename: string name of the file to be written output_lines: string the string to be written """ if pysic.get_cpu_id() == 0: # if we run MPI, only write with the master CPU f = file(filename,'a') f.write(output_lines) f.close()