def __init__(self, atoms=None): """ Parameters: atoms: ASE Atoms The full system. You can later on define it with set_atoms, or provide it directly in get_potential_energy()/get_forces(). """ self.total_timer = Timer(["total"]) self.total_timer.start("total") self.subsystem_energies = {} self.subsystems = {} self.subsystem_interactions = {} self.subsystem_info = {} self.interaction_info = {} self.forces = None self.stress = None self.potential_energy = None self.system_initialized = False if atoms is not None: self.atoms = atoms.copy() else: self.atoms = None
def __init__( self, full_system, primary_subsystem, secondary_subsystem, info): """ Parameters: full_system: ASE Atoms primary_subsystem: :class:`~pysic.subsystem.SubSystem` secondary_subsystem: :class:`~pysic.subsystem.SubSystem` info: :class:`~pysic.interaction.Interaction` """ self.info = info self.full_system = full_system self.primary_subsystem = primary_subsystem self.secondary_subsystem = secondary_subsystem self.uncorrected_interaction_energy = None self.uncorrected_interaction_forces = None self.link_atom_correction_energy = None self.link_atom_correction_forces = None self.interaction_energy = None self.interaction_forces = None # Determine if any potentials have been set self.has_interaction_potentials = False if self.info.comb_potential_enabled: self.has_interaction_potentials = True if self.info.coulomb_potential_enabled: self.has_interaction_potentials = True if len(self.info.potentials) != 0: self.has_interaction_potentials = True self.calculator = Pysic() self.pbc_calculator = Pysic() self.timer = Timer([ "Interaction", "Interaction (PBC)", "Forces", "Forces (PBC)", "Link atom correction energy", "Link atom correction forces"]) # The interaction needs to know if PBC:s are on pbc = primary_subsystem.atoms_for_interaction.get_pbc() if pbc[0] or pbc[1] or pbc[2]: self.has_pbc = True else: self.has_pbc = False # Initialize hydrogen links self.link_atoms = None self.setup_hydrogen_links(info.links) # Store the number of atoms in different systems self.n_primary = len(primary_subsystem.atoms_for_interaction) self.n_secondary = len(secondary_subsystem.atoms_for_interaction) self.n_full = self.n_primary + self.n_secondary if self.link_atoms is not None: self.n_links = len(self.link_atoms) else: self.n_links = 0 # Initialize the COMB potential first (set_potentials(COMB) is used, # because it isn' the same as add_potential(COMB)) if info.comb_potential_enabled: self.setup_comb_potential() # Initialize the coulomb interaction if info.electrostatic_parameters is not None: self.setup_coulomb_potential() # Add the other potentials self.setup_potentials() # Can't enable link atom correction on system without link atoms if len(info.links) == 0: self.info.link_atom_correction_enabled = False
class InteractionInternal(object): """The internal version of the Interaction-class. This class is meant only for internal use, and should not be accessed by the end-user. Attributes: info: :class:'~Pysic.interaction.Interaction' Contains all the info about the interaction given by the user. full_system: ASE Atoms - primary_subsystem: :class:`~pysic.subsystem.SubSystem` - secondary_subsystem: :class:`~pysic.subsystem.SubSystem` - uncorrected_interaction_energy: float The interaction energy without the link atom correction. uncorrected_interaction_forces: numpy array The interaction forces without the link atom correction. link_atom_correction_energy: float - link_atom_correction_forces: numpy array - interaction_energy: float The total interaction energy = uncorrected_interaction_energy + link_atom_correction_energy interaction_forces: numpy array The total interaction forces = uncorrected_interaction_forces + link_atom_correction_forces has_interaction_potentials: bool True if any potentials are defined. calculator: :class:'~pysic.calculator.Pysic' The pysic calculator used for non-pbc systems. pbc_calculator: :class:'~pysic.calculator.Pysic' The pysic calculator used for pbc systems. timer: :class:'~pysic.utility.timer.Timer' Used for tracking time usage. has_pbc: bool - link_atoms: ASE Atoms Contains all the hydrogen link atoms. Needed when calculating link atom correction. n_primary: int Number of atoms in primary subsystem. n_secondary: int Number of atoms in secondary subsystem. n_full: int Number of atoms in full system. n_links: int Number of link atoms. """ def __init__( self, full_system, primary_subsystem, secondary_subsystem, info): """ Parameters: full_system: ASE Atoms primary_subsystem: :class:`~pysic.subsystem.SubSystem` secondary_subsystem: :class:`~pysic.subsystem.SubSystem` info: :class:`~pysic.interaction.Interaction` """ self.info = info self.full_system = full_system self.primary_subsystem = primary_subsystem self.secondary_subsystem = secondary_subsystem self.uncorrected_interaction_energy = None self.uncorrected_interaction_forces = None self.link_atom_correction_energy = None self.link_atom_correction_forces = None self.interaction_energy = None self.interaction_forces = None # Determine if any potentials have been set self.has_interaction_potentials = False if self.info.comb_potential_enabled: self.has_interaction_potentials = True if self.info.coulomb_potential_enabled: self.has_interaction_potentials = True if len(self.info.potentials) != 0: self.has_interaction_potentials = True self.calculator = Pysic() self.pbc_calculator = Pysic() self.timer = Timer([ "Interaction", "Interaction (PBC)", "Forces", "Forces (PBC)", "Link atom correction energy", "Link atom correction forces"]) # The interaction needs to know if PBC:s are on pbc = primary_subsystem.atoms_for_interaction.get_pbc() if pbc[0] or pbc[1] or pbc[2]: self.has_pbc = True else: self.has_pbc = False # Initialize hydrogen links self.link_atoms = None self.setup_hydrogen_links(info.links) # Store the number of atoms in different systems self.n_primary = len(primary_subsystem.atoms_for_interaction) self.n_secondary = len(secondary_subsystem.atoms_for_interaction) self.n_full = self.n_primary + self.n_secondary if self.link_atoms is not None: self.n_links = len(self.link_atoms) else: self.n_links = 0 # Initialize the COMB potential first (set_potentials(COMB) is used, # because it isn' the same as add_potential(COMB)) if info.comb_potential_enabled: self.setup_comb_potential() # Initialize the coulomb interaction if info.electrostatic_parameters is not None: self.setup_coulomb_potential() # Add the other potentials self.setup_potentials() # Can't enable link atom correction on system without link atoms if len(info.links) == 0: self.info.link_atom_correction_enabled = False def setup_hydrogen_links(self, links): """Setup the hydrogen link atoms to the primary system. Parameters: link_parameters: list of tuples Contains the link atom parameters from the Interaction-object. Each tuple in the list is a link atom specification for a covalent bond of different type. The first item in the tuple is a list of tuples containing atom index pairs. The second item in the tuple is the CHL parameter for these links. """ self.link_atoms = Atoms(pbc=copy(self.full_system.get_pbc()), cell=copy(self.full_system.get_cell())) for bond in links: pairs = bond[0] CHL = bond[1] for link in pairs: # Extract the position of the boundary atoms q1_index = link[0] m1_index = link[1] iq1 = self.primary_subsystem.index_map.get(q1_index) im1 = self.secondary_subsystem.index_map.get(m1_index) if iq1 is None: error(("Invalid link: "+str(q1_index)+"-"+str(m1_index)+":\n" "The first index does not point to an atom in the primary system.")) if im1 is None: error(("Invalid link: "+str(q1_index)+"-"+str(m1_index)+":\n" "The second index does not point to an atom in the secondary system.")) q1 = self.primary_subsystem.atoms_for_subsystem[iq1] rq1 = q1.position # Calculate the separation between the host atoms from the full # system. We need to do this in the full system because the # subsystems may not have the same coordinate systems due to cell # size minimization. frq1 = self.full_system[q1_index].position frm1 = self.full_system[m1_index].position distance = CHL*(frm1 - frq1) # Calculate position for the hydrogen atom in both the primary # subsystem and in the system containing only link atoms. The link # atom system is used when calculating link atom corrections. r_primary = rq1 + distance r_link = frq1 + distance # Create a hydrogen atom in the primary subsystem and in the # full subsystem hydrogen_primary = Atom('H', position=r_primary) hydrogen_link = Atom('H', position=r_link) self.primary_subsystem.atoms_for_subsystem.append(hydrogen_primary) self.link_atoms.append(hydrogen_link) # Update the cell size after adding link atoms if self.primary_subsystem.cell_size_optimization_enabled: self.primary_subsystem.optimize_cell() def update_hydrogen_link_positions(self): """Used to update the position of the hydrogen link atoms specified in this interaction It is assumed that the position of the host atoms have already been updated. """ print "UPDATE" counter = 0 for bond in self.info.links: pairs = bond[0] CHL = bond[1] for i, link in enumerate(pairs): # Extract the position of the boundary atoms q1_index = link[0] m1_index = link[1] iq1 = self.primary_subsystem.index_map.get(q1_index) q1 = self.primary_subsystem.atoms_for_subsystem[iq1] rq1 = q1.position # Calculate the separation between the host atoms from the full # system. We need to do this in the full system because the # subsystems may not have the same coordinate systems due to # cell size minimization. frq1 = self.full_system[q1_index].position frm1 = self.full_system[m1_index].position print frq1 print frm1 distance = CHL*(frm1 - frq1) # Calculate position for the hydrogen atom in both the primary # subsystem and in the system containing only link atoms. The # link atom system is used when calculating link atom # corrections. r_primary = rq1 + distance r_link = frq1 + distance # Update hydrogen atom positions j = i + counter self.link_atoms[j].position = r_link self.primary_subsystem.atoms_for_subsystem[self.n_primary+j].position = r_primary # Add the number of links in this bond type to the counter counter += len(pairs) def setup_coulomb_potential(self): """Setups a Coulomb potential between the subsystems. Ewald calculation is automatically used for pbc-systems. Non-pbc systems use the ProductPotential to reproduce the Coulomb potential. """ parameters = self.info.electrostatic_parameters # Check that that should Ewald summation be used and if so, that all the # needed parameters are given if self.has_pbc: needed = ["k_cutoff", "real_cutoff", "sigma"] for param in needed: if param not in parameters: error(param + " not specified in Interaction.enable_coulomb_potential(). It is needed in order to calculate electrostatic interaction energy with Ewald summation in systems with periodic boundary conditions.") return ewald = CoulombSummation() ewald.set_parameter_value('epsilon', parameters['epsilon']) ewald.set_parameter_value('k_cutoff', parameters['k_cutoff']) ewald.set_parameter_value('real_cutoff', parameters['real_cutoff']) ewald.set_parameter_value('sigma', parameters['sigma']) self.pbc_calculator.set_coulomb_summation(ewald) else: # Add coulomb force between all charged particles in secondary # system, and all atoms in primary system. It is assumed that the # combined system will be made so that the primary system comes # before the secondary in indexing. coulomb_pairs = [] for i, ai in enumerate(self.primary_subsystem.atoms_for_interaction): for j, aj in enumerate(self.secondary_subsystem.atoms_for_interaction): if not np.allclose(aj.charge, 0): coulomb_pairs.append([i, j+self.n_primary]) # There are no charges in the secondary system, and that can't # change unlike the charges in primary system if len(coulomb_pairs) is 0: warn("There cannot be electrostatic interaction between the " "subsystems, because the secondary system does not have " "any initial charges", 2) return # The first potential given to the productpotential defines the # targets and cutoff kc = 1.0/(4.0*np.pi*parameters['epsilon']) max_cutoff = np.linalg.norm(np.array(self.primary_subsystem.atoms_for_interaction.get_cell())) coul1 = Potential('power', indices=coulomb_pairs, parameters=[1, 1, 1], cutoff=max_cutoff) coul2 = Potential('charge_pair', parameters=[kc, 1, 1]) coulomb_potential = ProductPotential([coul1, coul2]) self.calculator.add_potential(coulomb_potential) self.has_coulomb_interaction = True def setup_comb_potential(self): """Setups a COMB-potential between the subsystems. """ COMB = CombPotential(excludes=[]) COMB.set_calculator(self.pbc_calculator, True) # Notice that set_potentials is used here instead of add_potential. # This means that enable_comb_potential has to be called first in the # constructor. self.pbc_calculator.set_potentials(COMB) def setup_potentials(self): """Setups the additional Pysic potentials given in the Interaction-object. """ # If pbcs are not on, the targets of the potential are modified and the # calculator for finite systems is used if not self.has_pbc: primary_atoms = self.primary_subsystem.atoms_for_interaction secondary_atoms = self.secondary_subsystem.atoms_for_interaction # Make the interaction potentials for potential in self.info.potentials: symbols = potential.get_symbols() for pair in symbols: element1 = pair[0] element2 = pair[1] pairs = [] for i_a, a in enumerate(primary_atoms): for i_b, b in enumerate(secondary_atoms): if (a.symbol == element1 and b.symbol == element2) or (a.symbol == element2 and b.symbol == element2): pairs.append([i_a, i_b]) trimmed_potential = copy(potential) trimmed_potential.set_symbols(None) trimmed_potential.set_indices(pairs) self.calculator.add_potential(trimmed_potential) # If pbcs are on, the interactions need to be calculated with pbc # calculator else: for potential in self.info.potentials: self.pbc_calculator.add_potential(potential) def calculate_link_atom_correction_energy(self): """Calculates the link atom interaction energy defined as .. math:: E^\\text{link} = -E^\\text{tot}_\\text{MM}(\\text{HL})-E^\\text{int}_\\text{MM}(\\text{PS, HL}) """ self.timer.start("Link atom correction energy") primary_atoms = self.primary_subsystem.atoms_for_interaction link_atoms = self.link_atoms primary_and_link_atoms = primary_atoms + link_atoms secondary_calculator = self.secondary_subsystem.calculator # The Pysic calculators in one simulation all share one CoreMirror # object that contains the data about the Atoms. This object should be # automatically updated if the number of atoms changes. This is however # not happening for some reason, so we temporarily force the updation here. TODO: # Find out why this is the case secondary_calculator.force_core_initialization = True E1 = secondary_calculator.get_potential_energy(link_atoms) E2 = secondary_calculator.get_potential_energy(primary_and_link_atoms) E3 = secondary_calculator.get_potential_energy(primary_atoms) secondary_calculator.force_core_initialization = False link_atom_correction_energy = -E1 - (E2 - E1 - E3) self.link_atom_correction_energy = link_atom_correction_energy self.timer.stop() return copy(self.link_atom_correction_energy) def calculate_link_atom_correction_forces(self): """Calculates the link atom correction forces defined as .. math:: F^\\text{link} = -\\nabla(-E^\\text{tot}_\\text{MM}(\\text{HL})-E^\\text{int}_\\text{MM}(\\text{PS, HL})) """ self.timer.start("Link atom correction forces") primary_atoms = self.primary_subsystem.atoms_for_interaction link_atoms = self.link_atoms primary_and_link_atoms = primary_atoms + link_atoms secondary_calculator = self.secondary_subsystem.calculator # The Pysic calculators in one simulation all share one CoreMirror # object that contains the data about the Atoms. This object should be # automatically updated if the number of atoms changes. This is however # not happening for some reason, so we temporarily force the updation here. TODO: # Find out why this is the case secondary_calculator.force_core_initialization = True # The force arrays from the individual subsystems need to be extended # to the size of the combined system forces = np.zeros((len(primary_and_link_atoms), 3)) primary_postfix = np.zeros((len(link_atoms), 3)) link_prefix = np.zeros((len(primary_atoms), 3)) # Calculate the force that binds the link atoms and primary atoms. We # don't need to calculate the force between the link atoms, although # the associated energy had to be calculated. F1 = secondary_calculator.get_forces(link_atoms) F2 = secondary_calculator.get_forces(primary_and_link_atoms) F3 = secondary_calculator.get_forces(primary_atoms) secondary_calculator.force_core_initialization = False forces += F2 forces -= np.concatenate((F3, primary_postfix), axis=0) forces -= np.concatenate((link_prefix, F1), axis=0) # Ignore the forces acting on link atoms, and negate the force # direction. The energy can't be ignored, force can forces = -forces[0:self.n_primary, :] # Store the forces in a suitable numpy array that can be added to the # forces of the whole system link_atom_correction_forces = np.zeros((self.n_full, 3)) for sub_index in range(self.n_primary): full_index = self.primary_subsystem.reverse_index_map[sub_index] force = forces[sub_index, :] link_atom_correction_forces[full_index, :] = force self.link_atom_correction_forces = link_atom_correction_forces self.timer.stop() return copy(link_atom_correction_forces) def calculate_uncorrected_interaction_energy(self): """Calculates the interaction energy of a non-pbc system without the link atom correction. """ self.timer.start("Interaction") finite_energy = 0 primary = self.primary_subsystem.atoms_for_interaction secondary = self.secondary_subsystem.atoms_for_interaction combined = primary + secondary finite_energy = self.calculator.get_potential_energy(combined) self.uncorrected_interaction_energy = finite_energy self.timer.stop() return copy(finite_energy) def calculate_uncorrected_interaction_energy_pbc(self): """Calculates the interaction energy of a pbc system without the link atom correction. """ self.timer.start("Interaction (PBC)") pbc_energy = 0 primary = self.primary_subsystem.atoms_for_interaction secondary = self.secondary_subsystem.atoms_for_interaction combined = primary + secondary pbc_energy += self.pbc_calculator.get_potential_energy(combined) pbc_energy -= self.pbc_calculator.get_potential_energy(primary) pbc_energy -= self.pbc_calculator.get_potential_energy(secondary) self.uncorrected_interaction_energy = pbc_energy self.timer.stop() return copy(pbc_energy) def calculate_uncorrected_interaction_forces(self): """Calculates the interaction forces of a non-pbc system without the link atom correction. """ self.timer.start("Forces") primary = self.primary_subsystem.atoms_for_interaction.copy() secondary = self.secondary_subsystem.atoms_for_interaction.copy() combined_system = primary + secondary ## Forces due to finite coulomb potential and other pysic potentials forces = np.array(self.calculator.get_forces(combined_system)) self.uncorrected_interaction_forces = forces self.timer.stop() return copy(forces) def calculate_uncorrected_interaction_forces_pbc(self): """Calculates the interaction forces of a pbc system without the link atom correction. """ self.timer.start("Forces (PBC)") primary = self.primary_subsystem.atoms_for_interaction secondary = self.secondary_subsystem.atoms_for_interaction combined = primary + secondary primary_postfix = np.zeros((self.n_secondary, 3)) secondary_prefix = np.zeros((self.n_primary, 3)) # The force arrays from the individual subsystems need to be extended # to the size of the combined system forces = self.pbc_calculator.get_forces(combined) forces -= np.concatenate((self.pbc_calculator.get_forces(primary), primary_postfix), axis=0) forces -= np.concatenate((secondary_prefix, self.pbc_calculator.get_forces(secondary)), axis=0) self.uncorrected_interaction_forces = forces self.timer.stop() return copy(forces) def update_subsystem_charges(self): """Updates the charges in the subsystems involved in the interaction (if charge update is enabled in them). """ if self.info.coulomb_potential_enabled: self.primary_subsystem.update_charges() self.secondary_subsystem.update_charges() def get_interaction_energy(self): """Returns the total interaction energy which consists of the uncorrected energies and possible the link atom correction. Returns: float: the interaction energy """ # Try to update the charges self.update_subsystem_charges() interaction_energy = 0 # Calculate the interaction energies if self.has_interaction_potentials: if self.has_pbc: interaction_energy += self.calculate_uncorrected_interaction_energy_pbc() else: interaction_energy += self.calculate_uncorrected_interaction_energy() # Calculate the link atom correction energy if needed if self.info.link_atom_correction_enabled is True: interaction_energy += self.calculate_link_atom_correction_energy() self.interaction_energy = interaction_energy return copy(self.interaction_energy) def get_interaction_forces(self): """Return a numpy array of total 3D forces for each atom in the whole system. The forces consists of the uncorrected forces and possibly the link atom correction forces. The row index refers to the atom index in the original structure. Returns: numpy array: the forces for each atom in the full system """ # Try to update the charges self.update_subsystem_charges() interaction_forces = np.zeros((self.n_full, 3)) # Calculate the uncorrected interaction forces if self.has_interaction_potentials: if self.has_pbc: interaction_forces += self.calculate_uncorrected_interaction_forces_pbc() else: interaction_forces += self.calculate_uncorrected_interaction_forces() # Calculate the link atom correction forces if needed if self.info.link_atom_correction_enabled is True: interaction_forces += self.calculate_link_atom_correction_forces() self.interaction_forces = interaction_forces return copy(self.interaction_forces)
def __init__(self, full_system, primary_subsystem, secondary_subsystem, info): """ Parameters: full_system: ASE Atoms primary_subsystem: :class:`~pysic.subsystem.SubSystem` secondary_subsystem: :class:`~pysic.subsystem.SubSystem` info: :class:`~pysic.interaction.Interaction` """ self.info = info self.full_system = full_system self.primary_subsystem = primary_subsystem self.secondary_subsystem = secondary_subsystem self.uncorrected_interaction_energy = None self.uncorrected_interaction_forces = None self.link_atom_correction_energy = None self.link_atom_correction_forces = None self.interaction_energy = None self.interaction_forces = None # Determine if any potentials have been set self.has_interaction_potentials = False if self.info.comb_potential_enabled: self.has_interaction_potentials = True if self.info.coulomb_potential_enabled: self.has_interaction_potentials = True if len(self.info.potentials) != 0: self.has_interaction_potentials = True self.calculator = Pysic() self.pbc_calculator = Pysic() self.timer = Timer( [ "Interaction", "Interaction (PBC)", "Forces", "Forces (PBC)", "Link atom correction energy", "Link atom correction forces", ] ) # The interaction needs to know if PBC:s are on pbc = primary_subsystem.atoms_for_interaction.get_pbc() if pbc[0] or pbc[1] or pbc[2]: self.has_pbc = True else: self.has_pbc = False # Initialize hydrogen links self.link_atoms = None self.setup_hydrogen_links(info.links) # Store the number of atoms in different systems self.n_primary = len(primary_subsystem.atoms_for_interaction) self.n_secondary = len(secondary_subsystem.atoms_for_interaction) self.n_full = self.n_primary + self.n_secondary if self.link_atoms is not None: self.n_links = len(self.link_atoms) else: self.n_links = 0 # Initialize the COMB potential first (set_potentials(COMB) is used, # because it isn' the same as add_potential(COMB)) if info.comb_potential_enabled: self.setup_comb_potential() # Initialize the coulomb interaction if info.electrostatic_parameters is not None: self.setup_coulomb_potential() # Add the other potentials self.setup_potentials() # Can't enable link atom correction on system without link atoms if len(info.links) == 0: self.info.link_atom_correction_enabled = False
class HybridCalculator(object): """Used to create and perform hybrid calculations. This class is a fully compatible ASE calculator that provides the possibility of dividing the Atoms object into subsystems with different ASE calculators attached to them. You can also define hydrogen link atoms in the interfaces of these subsystems or define any Pysic Potentials through which the subsystems interact with each other. Attributes: total_timer: :class:'~pysic.utility.timer.Timer' Keeps track of the total time spent in the calculations. subsystem_energies: dictionary Name to energy. subsystems: dictionary Name to SubSystemInternal. subsystem_interactions: dictionary Pair of names to InteractionInternal. subsystem_info: dictionary Name to SubSystem. interaction_info: dictionary Pair of names to Interaction. forces: numpy array - stress: None - potential_energy: float - system_initialized: bool Indicates whether the SubSystemInternals and InteractionInternals have been constructed. self.atoms: ASE Atoms The full system. """ #--------------------------------------------------------------------------- # Hybrid methods def __init__(self, atoms=None): """ Parameters: atoms: ASE Atoms The full system. You can later on define it with set_atoms, or provide it directly in get_potential_energy()/get_forces(). """ self.total_timer = Timer(["total"]) self.total_timer.start("total") self.subsystem_energies = {} self.subsystems = {} self.subsystem_interactions = {} self.subsystem_info = {} self.interaction_info = {} self.forces = None self.stress = None self.potential_energy = None self.system_initialized = False if atoms is not None: self.atoms = atoms.copy() else: self.atoms = None def set_atoms(self, atoms): """Set the full system for hybrid calculations. Use :meth:`~pysic.hybridcalculation.add_subsystem` for setting up the subsystems. Parameters: atoms: ASE Atoms The full system. """ # Update the self.atoms and subsystems if necessary if not self.identical_atoms(atoms): self.update_system(atoms) # Initialize the subsystems and interactions if necessary if self.system_initialized is False: self.initialize_system() def get_atoms(self): """Return a copy of the full system.""" return self.atoms.copy() def add_subsystem(self, subsystem): """Used to define subsystems You can define a subsystem with a oneliner, or then call setters on the SubSystemInfo object returned by this function. Parameters: subsystem: SubSystem object """ if subsystem.name in self.subsystem_info: warn("Overriding an existing subsystem", 2) self.subsystem_info[subsystem.name] = subsystem def add_interaction(self, interaction): """Used to add a interaction between two subsystems. Parameters: interaction: Interaction The Interaction object containing the information. """ primary = interaction.primary secondary = interaction.secondary # Check that the subsystems exist if not self.subsystem_defined(primary): return if not self.subsystem_defined(secondary): return self.interaction_info[(interaction.primary, interaction.secondary)] = interaction def initialize_system(self): """ Initializes the subsystems and interactions. Called once during the lifetime of the calculator. Typically when calling set_atoms for the first time, or when calculating any quantity for the first time. If the atoms in the simulation need to be updated, update_system() is used. """ # Can't do calculation without atoms if not self.full_system_set(): return # Initialize subsystems for subsystem_info in self.subsystem_info.itervalues(): self.initialize_subsystem(subsystem_info) # Check that the subsystems cover the entire system if len(self.get_unsubsystemized_atoms()) is not 0: warn("Subsystems do not cover the entire system", 2) self.system_initialized = True return # Initialize interactions for interaction_info in self.interaction_info.itervalues(): self.initialize_interaction(interaction_info) self.system_initialized = True def initialize_interaction(self, info): """Initializes a InteractionInternal from the given Interaction. Parameters: info: Interaction """ primary = info.primary secondary = info.secondary # Check subsystem existence if not self.subsystem_defined(primary): warn("The given subsystem "+primary+" does not exist.", 2) return if not self.subsystem_defined(secondary): warn("The given subsystem "+secondary+" does not exist.", 2) return interaction = InteractionInternal( self.atoms, self.subsystems[primary], self.subsystems[secondary], info) self.subsystem_interactions[(primary, secondary)] = interaction def initialize_subsystem(self, info): """Initializes a SubsystemInternal from the given Subsystem. Parameters: info: SubSystem """ name = info.name real_indices = self.generate_subsystem_indices(name) # Create a copy of the subsystem temp_atoms = Atoms() index_map = {} reverse_index_map = {} counter = 0 for index in real_indices: atom = self.atoms[index] temp_atoms.append(atom) index_map[index] = counter reverse_index_map[counter] = index counter += 1 atoms = temp_atoms.copy() atoms.set_pbc(self.atoms.get_pbc()) atoms.set_cell(self.atoms.get_cell()) # Create the SubSystem subsystem = SubSystemInternal( atoms, info, index_map, reverse_index_map, len(self.atoms)) self.subsystems[name] = subsystem def update_system(self, atoms): """Update the subsystem atoms. """ # Replace the old internal atoms self.atoms = atoms.copy() # Update the positions in the subsystems. If link atoms are present, # they are also moved. The velocities and momenta are not updated in # the subsystems. for subsystem in self.subsystems.values(): for full_index, sub_index in subsystem.index_map.iteritems(): new_position = self.atoms[full_index].position subsystem.atoms_for_interaction[sub_index].position = copy.copy(new_position) subsystem.atoms_for_subsystem[sub_index].position = copy.copy(new_position) # Update the link atom positions for interaction in self.subsystem_interactions.values(): interaction.full_system = self.atoms if len(interaction.info.links) != 0: interaction.update_hydrogen_link_positions() def check_subsystem_indices(self, atom_indices, name): """Check that the atomic indices of the subsystem are present.""" if not self.full_system_set(): return False n_atoms = len(self.atoms) for index in atom_indices: if index > n_atoms or index < 0: warn("The subsystem \"" + name + "\" contains nonexisting atoms", 2) return False return True def subsystem_defined(self, name): """Checks that there is a subsystem with the given name. """ defined = name in self.subsystem_info if not defined: warn("The subsystem called \"" + name + "\" has not been defined", 2) return defined def full_system_set(self): """Checks whether the full system has been defined. """ is_set = self.atoms is not None if not is_set: warn("No Atoms object given to the calculator", 2) return is_set def get_unsubsystemized_atoms(self): """Return a list of indices for the atoms not already in a subsystem. """ n_atoms = len(self.atoms) used_indices = [] unsubsystemized_atoms = [] for subsystem in self.subsystems.values(): for index in subsystem.index_map.keys(): used_indices.append(index) for index in range(n_atoms): if index not in used_indices: unsubsystemized_atoms.append(index) return unsubsystemized_atoms def get_subsystem_indices(self, name): """Return the indices of the atoms in the subsystem in the full system. You can ask the indices even if the subsystems have not been initialized, but the indices of different subsystems may overlap in this case. If the subsystems have been initialized this function will only return indices if they are valid. """ # Check that the atoms have been set if not self.full_system_set: return # Check that the subsystem has been defined if not self.subsystem_defined(name): return indices = self.subsystem_info[name].real_indices if indices is None: return self.generate_subsystem_indices(name) def generate_subsystem_indices(self, name): """Generates the indices for the given subsystem. """ # Check that the subsystem exists if not self.subsystem_defined(name): return # Check that the atoms have been set if not self.full_system_set(): return info = self.subsystem_info[name] indices = info.indices tag = info.tag # Determine how the atoms are specified: indices or tag if indices == "remaining": real_indices = self.get_unsubsystemized_atoms() elif (indices is not None) and (tag is None): real_indices = indices elif (indices is None) and (tag is not None): real_indices = [] for i, t in enumerate(self.atoms.get_tags()): if t == tag: real_indices.append(i) # Check whether the subsystem contains any atoms: if len(real_indices) is 0: warn("The specified subsystem " + "\""+name+"\"" + " does not contain any atoms.", 2) return # Check subsystem indices and overlap if self.check_subsystem_indices(real_indices, name): if not self.check_subsystem_overlap(real_indices, name): return real_indices def check_subsystem_overlap(self, atom_indices, name): """Check that the subsystem doesn't overlap with another one. """ if self.subsystems is None: return False new_indices = [] new_indices += atom_indices for subsystem in self.subsystems.values(): new_indices += subsystem.index_map.keys() if len(new_indices) > len(set(new_indices)): warn("The subsystem \""+name+"\" overlaps with another system", 2) return True return False def calculate_potential_energy(self): """Calculates the potential energy in the current structure. """ total_potential_energy = 0 # The connection initialization may alter the subsystem # (e.g. add a hydrogen link atom) so make sure that the connections are # initialized before using any calculators for name, subsystem in self.subsystems.iteritems(): subsystem_energy = subsystem.get_potential_energy() total_potential_energy += subsystem_energy # Calculate connection energies after the subsystem energies. This way # the pseudo_density is already calculated for the primary system for interaction in self.subsystem_interactions.values(): total_potential_energy += interaction.get_interaction_energy() self.potential_energy = total_potential_energy def calculate_forces(self): """Calculates the forces in the current structure. This force includes the forces internal to the subsystems and the forces that bind the subsystems together. """ forces = np.zeros((len(self.atoms), 3)) # Calculate the forces internal to the subsystems. These need to be # calculated first, so that the pseudo density and grid are available # for Interactions for subsystem in self.subsystems.values(): forces += subsystem.get_forces() # Calculate the interaction forces for interaction in self.subsystem_interactions.values(): forces += interaction.get_interaction_forces() self.forces = forces def identical_atoms(self, atoms): """Compares the given atoms to the stored atoms object. The Atoms are identical if the positions, atomic numbers, periodic boundary conditions and cell are the same. """ # If one of the atoms is None, return false if self.atoms is None or atoms is None: return False # Check if the atoms are identical if not np.array_equal(self.atoms.get_positions(), atoms.get_positions()): return False if not np.array_equal(self.atoms.get_atomic_numbers(), atoms.get_atomic_numbers()): return False if not np.array_equal(self.atoms.get_pbc(), atoms.get_pbc()): return False if not np.array_equal(self.atoms.get_cell(), atoms.get_cell()): return False return True #--------------------------------------------------------------------------- # ASE Calculator interface functions def calculation_required(self, atoms, quantities=['forces', 'energy', 'stress']): """Check if a calculation is required for any of the the given properties. Check if the quantities in the quantities list have already been calculated for the atomic configuration atoms. The quantities can be one or more of: 'energy', 'forces', 'stress'. This method is used to check if a quantity is available without further calculations. For this reason, calculators should react to unknown/unsupported quantities by returning True, indicating that the quantity is not available. Two sets of atoms are considered identical if they have the same positions, atomic numbers, unit cell and periodic boundary conditions. Parameters: atoms: ASE Atoms This structure is compared to the currently stored. quantities: list of strings list of keywords 'energy', 'forces', 'stress' Returns: bool: True if the quantities need to be calculated, false otherwise. """ # See if internal atoms are present, are atoms given as parameter and # whether the given atoms and stored atoms are identical if self.atoms is None: if atoms is not None: return True else: warn(("No Atoms object given to the calculator. Please provide" "atoms as an argument, or use set_atoms()"), 2) return True elif atoms is not None: if self.identical_atoms(atoms) is False: return True # Check if the wanted quantities are already calculated if type(quantities) is not list: quantities = [quantities] for quantity in quantities: if quantity == 'energy': if self.potential_energy is None: return True elif quantity == 'forces': if self.forces is None: return True elif quantity == 'stress': if self.stress is None: return True else: return True return False def get_potential_energy(self, atoms=None, force_consistent=False): """Returns the potential energy of the hybrid system. """ # Can't do force_consistent calculations if force_consistent: warn("Can't do force consistent calculations. Returning the energy extrapolated to zero kelvin.", 2) # Can't do calculation without atoms if self.atoms is None and atoms is None: warn(("No Atoms object given to the calculator. " "Please provide atoms as an argument, or use set_atoms()"), 2) # See if calculation is required if self.calculation_required(atoms, 'energy'): # Update the system if necessary if atoms is not None: if not self.identical_atoms(atoms): self.update_system(atoms) # Initialize the subsystems and interactions if necessary if self.system_initialized is False: self.initialize_system() # Calculate the potential energy and store to self.potential_energy self.calculate_potential_energy() return np.copy(self.potential_energy) def get_forces(self, atoms=None): """Returns the forces acting on the atoms. If the atoms parameter is given, it will be used for updating the structure assigned to the calculator prior to calculating the forces. Otherwise the structure already associated with the calculator is used. The calculator checks if the forces have been calculated already via :meth:`~pysic.hybridcalculator.HybridCalculator.calculation_required`. If the structure has changed, the forces are calculated using :meth:`~pysic.hybridcalculator.HybridCalculator.calculate_forces` Parameters: atoms: ASE Atoms object The structure to calculate the forces on. """ # Can't do calculation without atoms if self.atoms is None and atoms is None: warn(("No Atoms object given to the calculator. " "Please provide atoms as an argument, or use set_atoms()"), 2) # See if calculation is required if self.calculation_required(atoms, 'forces'): # Update the system if necessary if atoms is not None: if not self.identical_atoms(atoms): self.update_system(atoms) # Initialize the subsystems and interactions if necessary if self.system_initialized is False: self.initialize_system() # Calculate the forces and store to self.forces self.calculate_forces() return np.copy(self.forces) def get_stress(self, atoms=None, skip_charge_relaxation=False): """This function has not been implemented. """ warn("Stress has no been implemented yet.", 2) return None #--------------------------------------------------------------------------- # Miscellanous utility functions def view_subsystems(self): """Views the subsystems with ASE:s built in viewer. """ # Initialize the system if necessary if not self.system_initialized: self.initialize_system() if rank == 0: for subsystem in self.subsystems.values(): view(subsystem.atoms_for_subsystem) def get_subsystem_pseudo_density(self, name): """Returns the electron pseudo density for the given subsystem. """ if self.subsystem_defined(name): return self.subsystems[name].get_pseudo_density() def calculate_subsystem_interaction_charges(self, name): """Returns the calculated interaction charges for the given subsystem. Before the charges can be calulated, one energy calculation has to be made so that the electron density is available """ if self.subsystem_defined(name): # Initialize the subsystems and interactions if necessary if not self.system_initialized: self.initialize_system() self.subsystems[name].get_potential_energy() self.subsystems[name].update_charges() def print_interaction_charge_summary(self): """Print a summary of the atomic charges that are used in the electrostatic interaction between subsystems. """ message = [] for name, subsystem in self.subsystems.iteritems(): message.append("Subsystem \"" + name + "\":") for i_atom, atom in enumerate(subsystem.atoms_for_interaction): symbol = atom.symbol charge = atom.charge message.append(" Number: " + str(i_atom) + ", Symbol: " + symbol + ", Charge: " + str(charge)) message.append("") str_message = style_message("HYBRIDCALCULATOR SUMMARY OF CHARGES USED IN INTERACTIONS", message) parprint(str_message) def print_energy_summary(self): """Print a detailed summary of the different energies in the system. This includes the energies in the subsystems, interaction energies and possible energy corrections. """ message = [] message.append("Total energy: "+str(self.potential_energy)) message.append("") for name, subsystem in self.subsystems.iteritems(): message.append("Subsystem \"" + name + "\":") if subsystem.potential_energy is None: ss_energy = "Not calculated" else: ss_energy = str(subsystem.potential_energy) message.append(" Potential energy: " + ss_energy) message.append("") for pair, interaction in self.subsystem_interactions.iteritems(): message.append("Interaction between \""+pair[0]+"\" and \""+pair[1] + ":") # Total interaction energy if interaction.interaction_energy is None: b_energy = "Not calculated" else: b_energy = str(interaction.interaction_energy) message.append(" Total interaction energy: "+b_energy) # Link atom correction energy if interaction.link_atom_correction_energy is None: link_atom_correction_energy = "Not calculated" else: link_atom_correction_energy = str(interaction.link_atom_correction_energy) message.append(" Link atom correction energy: "+link_atom_correction_energy) str_message = style_message("HYBRIDCALCULATOR ENERGY SUMMARY", message) parprint(str_message) def print_time_summary(self): """Print a detailed summary of the time usage. """ if rank == 0: self.total_timer.stop() total_time = self.total_timer.get_total_time() known_time = 0 for interaction in self.subsystem_interactions.values(): known_time += interaction.timer.get_total_time() for subsystem in self.subsystems.values(): known_time += subsystem.timer.get_total_time() unknown_time = total_time - known_time message = [] for name, subsystem in self.subsystems.iteritems(): message.append("Subsystem \"" + name + "\":") subsystem_time = subsystem.timer.get_total_time() if np.isclose(subsystem_time, 0): message.append(" Time usage: 0 %") else: message.append(" Time usage: " + "{0:.1f}".format(subsystem_time/total_time*100.0) + " %") for name, time in subsystem.timer.sections.iteritems(): message.append(" " + name + ": " + "{0:.1f}".format(time/total_time*100.0) + " %") for pair, interaction in self.subsystem_interactions.iteritems(): message.append("Interaction between \""+pair[0]+"\" and \""+pair[1] + ":") interaction_time = interaction.timer.get_total_time() if np.isclose(interaction_time, 0): message.append(" Time usage: 0 %") else: message.append(" Time usage: " + "{0:.1f}".format(interaction_time/total_time*100.0) + " %") for name, time in interaction.timer.sections.iteritems(): message.append(" " + name + ": " + "{0:.1f}".format(time/total_time*100.0) + " %") message.append("Other tasks: " + "{0:.1f}".format(unknown_time/total_time*100.0) + " %") str_message = style_message("HYBRIDCALCULATOR TIME SUMMARY", message) print(str_message) def print_force_summary(self): """Print a detailed summary of forces in the system. """ message = [] message.append("Total forces:") if self.forces is None: message.append(" Not calculated") else: for i, force in enumerate(self.forces): message.append(" " + str(i) + ": " + str(force)) message.append("") # Forces in subsystems for name, subsystem in self.subsystems.iteritems(): message.append("Subsystem \"" + name + "\":") if subsystem.forces is None: message.append(" Not calculated") else: for i, force in enumerate(subsystem.forces): message.append(" " + str(i) + ": " + str(force)) message.append("") # Forces in interactions for pair, interaction in self.subsystem_interactions.iteritems(): message.append("Interaction forces between \""+pair[0]+"\" and \""+pair[1] + ":") # Total interaction force message.append(" Total:") if interaction.interaction_forces is None: message.append(" Not calculated") else: for i, force in enumerate(interaction.interaction_forces): message.append(" " + str(i) + ": " + str(force)) message.append("") # Link atom correction message.append(" Link atom correction:") if interaction.link_atom_correction_forces is None: message.append(" Not calculated") else: for i, force in enumerate(interaction.link_atom_correction_forces): message.append(" " + str(i) + ": " + str(force)) message.append("") str_message = style_message("HYBRIDCALCULATOR FORCE SUMMARY", message) parprint(str_message) def get_colors(self): """Returns a color set for AtomEyeViewer with different colours for the different subsystems. When the subsystems have been defined with add_subsystem() and the atoms have been set, this function will return a list of colors for each atom. The different subsystems have different colours for easy identification. You can provide this list of colors to an AtomEyeViewer object for visualization. """ # Initialize the system if necessary if not self.system_initialized: self.initialize_system() # Create the different colours for the subsystems n_subsystems = len(self.subsystems) hue_space = np.linspace(0.0, 2.0/3.0, n_subsystems) rgb_values = [] for hue in hue_space: rgb = colorsys.hls_to_rgb(hue, 0.5, 0.7) rgb_values.append(rgb) # Create the color list colors = np.zeros((len(self.atoms), 3), dtype=float) for i_ss, ss in enumerate(self.subsystems.values()): ss_color = np.array(rgb_values[i_ss]) index_map = ss.index_map for system_index in index_map: number = self.atoms[system_index].number atom_color = np.array(ase.data.colors.cpk_colors[number]) colors[system_index, :] = 0.1*atom_color+0.9*ss_color return colors.tolist() def get_subsystem(self, name): """Returns a copy of the ASE Atoms object for a certain subsystem. The returned subsystem can be used for e.g. visualization or debugging. """ # Check if name defined if self.subsystem_defined(name): # Initialize the system if necessary if not self.system_initialized: self.initialize_system() return self.subsystems[name].atoms_for_subsystem
class InteractionInternal(object): """The internal version of the Interaction-class. This class is meant only for internal use, and should not be accessed by the end-user. Attributes: info: :class:'~Pysic.interaction.Interaction' Contains all the info about the interaction given by the user. full_system: ASE Atoms - primary_subsystem: :class:`~pysic.subsystem.SubSystem` - secondary_subsystem: :class:`~pysic.subsystem.SubSystem` - uncorrected_interaction_energy: float The interaction energy without the link atom correction. uncorrected_interaction_forces: numpy array The interaction forces without the link atom correction. link_atom_correction_energy: float - link_atom_correction_forces: numpy array - interaction_energy: float The total interaction energy = uncorrected_interaction_energy + link_atom_correction_energy interaction_forces: numpy array The total interaction forces = uncorrected_interaction_forces + link_atom_correction_forces has_interaction_potentials: bool True if any potentials are defined. calculator: :class:'~pysic.calculator.Pysic' The pysic calculator used for non-pbc systems. pbc_calculator: :class:'~pysic.calculator.Pysic' The pysic calculator used for pbc systems. timer: :class:'~pysic.utility.timer.Timer' Used for tracking time usage. has_pbc: bool - link_atoms: ASE Atoms Contains all the hydrogen link atoms. Needed when calculating link atom correction. n_primary: int Number of atoms in primary subsystem. n_secondary: int Number of atoms in secondary subsystem. n_full: int Number of atoms in full system. n_links: int Number of link atoms. """ def __init__(self, full_system, primary_subsystem, secondary_subsystem, info): """ Parameters: full_system: ASE Atoms primary_subsystem: :class:`~pysic.subsystem.SubSystem` secondary_subsystem: :class:`~pysic.subsystem.SubSystem` info: :class:`~pysic.interaction.Interaction` """ self.info = info self.full_system = full_system self.primary_subsystem = primary_subsystem self.secondary_subsystem = secondary_subsystem self.uncorrected_interaction_energy = None self.uncorrected_interaction_forces = None self.link_atom_correction_energy = None self.link_atom_correction_forces = None self.interaction_energy = None self.interaction_forces = None # Determine if any potentials have been set self.has_interaction_potentials = False if self.info.comb_potential_enabled: self.has_interaction_potentials = True if self.info.coulomb_potential_enabled: self.has_interaction_potentials = True if len(self.info.potentials) != 0: self.has_interaction_potentials = True self.calculator = Pysic() self.pbc_calculator = Pysic() self.timer = Timer( [ "Interaction", "Interaction (PBC)", "Forces", "Forces (PBC)", "Link atom correction energy", "Link atom correction forces", ] ) # The interaction needs to know if PBC:s are on pbc = primary_subsystem.atoms_for_interaction.get_pbc() if pbc[0] or pbc[1] or pbc[2]: self.has_pbc = True else: self.has_pbc = False # Initialize hydrogen links self.link_atoms = None self.setup_hydrogen_links(info.links) # Store the number of atoms in different systems self.n_primary = len(primary_subsystem.atoms_for_interaction) self.n_secondary = len(secondary_subsystem.atoms_for_interaction) self.n_full = self.n_primary + self.n_secondary if self.link_atoms is not None: self.n_links = len(self.link_atoms) else: self.n_links = 0 # Initialize the COMB potential first (set_potentials(COMB) is used, # because it isn' the same as add_potential(COMB)) if info.comb_potential_enabled: self.setup_comb_potential() # Initialize the coulomb interaction if info.electrostatic_parameters is not None: self.setup_coulomb_potential() # Add the other potentials self.setup_potentials() # Can't enable link atom correction on system without link atoms if len(info.links) == 0: self.info.link_atom_correction_enabled = False def setup_hydrogen_links(self, links): """Setup the hydrogen link atoms to the primary system. Parameters: link_parameters: list of tuples Contains the link atom parameters from the Interaction-object. Each tuple in the list is a link atom specification for a covalent bond of different type. The first item in the tuple is a list of tuples containing atom index pairs. The second item in the tuple is the CHL parameter for these links. """ self.link_atoms = Atoms(pbc=copy(self.full_system.get_pbc()), cell=copy(self.full_system.get_cell())) for bond in links: pairs = bond[0] CHL = bond[1] for link in pairs: # Extract the position of the boundary atoms q1_index = link[0] m1_index = link[1] iq1 = self.primary_subsystem.index_map.get(q1_index) im1 = self.secondary_subsystem.index_map.get(m1_index) if iq1 is None: error( ( "Invalid link: " + str(q1_index) + "-" + str(m1_index) + ":\n" "The first index does not point to an atom in the primary system." ) ) if im1 is None: error( ( "Invalid link: " + str(q1_index) + "-" + str(m1_index) + ":\n" "The second index does not point to an atom in the secondary system." ) ) q1 = self.primary_subsystem.atoms_for_subsystem[iq1] rq1 = q1.position # Calculate the separation between the host atoms from the full # system. We need to do this in the full system because the # subsystems may not have the same coordinate systems due to cell # size minimization. frq1 = self.full_system[q1_index].position frm1 = self.full_system[m1_index].position distance = CHL * (frm1 - frq1) # Calculate position for the hydrogen atom in both the primary # subsystem and in the system containing only link atoms. The link # atom system is used when calculating link atom corrections. r_primary = rq1 + distance r_link = frq1 + distance # Create a hydrogen atom in the primary subsystem and in the # full subsystem hydrogen_primary = Atom("H", position=r_primary) hydrogen_link = Atom("H", position=r_link) self.primary_subsystem.atoms_for_subsystem.append(hydrogen_primary) self.link_atoms.append(hydrogen_link) # Update the cell size after adding link atoms if self.primary_subsystem.cell_size_optimization_enabled: self.primary_subsystem.optimize_cell() def update_hydrogen_link_positions(self): """Used to update the position of the hydrogen link atoms specified in this interaction It is assumed that the position of the host atoms have already been updated. """ print "UPDATE" counter = 0 for bond in self.info.links: pairs = bond[0] CHL = bond[1] for i, link in enumerate(pairs): # Extract the position of the boundary atoms q1_index = link[0] m1_index = link[1] iq1 = self.primary_subsystem.index_map.get(q1_index) q1 = self.primary_subsystem.atoms_for_subsystem[iq1] rq1 = q1.position # Calculate the separation between the host atoms from the full # system. We need to do this in the full system because the # subsystems may not have the same coordinate systems due to # cell size minimization. frq1 = self.full_system[q1_index].position frm1 = self.full_system[m1_index].position print frq1 print frm1 distance = CHL * (frm1 - frq1) # Calculate position for the hydrogen atom in both the primary # subsystem and in the system containing only link atoms. The # link atom system is used when calculating link atom # corrections. r_primary = rq1 + distance r_link = frq1 + distance # Update hydrogen atom positions j = i + counter self.link_atoms[j].position = r_link self.primary_subsystem.atoms_for_subsystem[self.n_primary + j].position = r_primary # Add the number of links in this bond type to the counter counter += len(pairs) def setup_coulomb_potential(self): """Setups a Coulomb potential between the subsystems. Ewald calculation is automatically used for pbc-systems. Non-pbc systems use the ProductPotential to reproduce the Coulomb potential. """ parameters = self.info.electrostatic_parameters # Check that that should Ewald summation be used and if so, that all the # needed parameters are given if self.has_pbc: needed = ["k_cutoff", "real_cutoff", "sigma"] for param in needed: if param not in parameters: error( param + " not specified in Interaction.enable_coulomb_potential(). It is needed in order to calculate electrostatic interaction energy with Ewald summation in systems with periodic boundary conditions." ) return ewald = CoulombSummation() ewald.set_parameter_value("epsilon", parameters["epsilon"]) ewald.set_parameter_value("k_cutoff", parameters["k_cutoff"]) ewald.set_parameter_value("real_cutoff", parameters["real_cutoff"]) ewald.set_parameter_value("sigma", parameters["sigma"]) self.pbc_calculator.set_coulomb_summation(ewald) else: # Add coulomb force between all charged particles in secondary # system, and all atoms in primary system. It is assumed that the # combined system will be made so that the primary system comes # before the secondary in indexing. coulomb_pairs = [] for i, ai in enumerate(self.primary_subsystem.atoms_for_interaction): for j, aj in enumerate(self.secondary_subsystem.atoms_for_interaction): if not np.allclose(aj.charge, 0): coulomb_pairs.append([i, j + self.n_primary]) # There are no charges in the secondary system, and that can't # change unlike the charges in primary system if len(coulomb_pairs) is 0: warn( "There cannot be electrostatic interaction between the " "subsystems, because the secondary system does not have " "any initial charges", 2, ) return # The first potential given to the productpotential defines the # targets and cutoff kc = 1.0 / (4.0 * np.pi * parameters["epsilon"]) max_cutoff = np.linalg.norm(np.array(self.primary_subsystem.atoms_for_interaction.get_cell())) coul1 = Potential("power", indices=coulomb_pairs, parameters=[1, 1, 1], cutoff=max_cutoff) coul2 = Potential("charge_pair", parameters=[kc, 1, 1]) coulomb_potential = ProductPotential([coul1, coul2]) self.calculator.add_potential(coulomb_potential) self.has_coulomb_interaction = True def setup_comb_potential(self): """Setups a COMB-potential between the subsystems. """ COMB = CombPotential(excludes=[]) COMB.set_calculator(self.pbc_calculator, True) # Notice that set_potentials is used here instead of add_potential. # This means that enable_comb_potential has to be called first in the # constructor. self.pbc_calculator.set_potentials(COMB) def setup_potentials(self): """Setups the additional Pysic potentials given in the Interaction-object. """ # If pbcs are not on, the targets of the potential are modified and the # calculator for finite systems is used if not self.has_pbc: primary_atoms = self.primary_subsystem.atoms_for_interaction secondary_atoms = self.secondary_subsystem.atoms_for_interaction # Make the interaction potentials for potential in self.info.potentials: symbols = potential.get_symbols() for pair in symbols: element1 = pair[0] element2 = pair[1] pairs = [] for i_a, a in enumerate(primary_atoms): for i_b, b in enumerate(secondary_atoms): if (a.symbol == element1 and b.symbol == element2) or ( a.symbol == element2 and b.symbol == element2 ): pairs.append([i_a, i_b]) trimmed_potential = copy(potential) trimmed_potential.set_symbols(None) trimmed_potential.set_indices(pairs) self.calculator.add_potential(trimmed_potential) # If pbcs are on, the interactions need to be calculated with pbc # calculator else: for potential in self.info.potentials: self.pbc_calculator.add_potential(potential) def calculate_link_atom_correction_energy(self): """Calculates the link atom interaction energy defined as .. math:: E^\\text{link} = -E^\\text{tot}_\\text{MM}(\\text{HL})-E^\\text{int}_\\text{MM}(\\text{PS, HL}) """ self.timer.start("Link atom correction energy") primary_atoms = self.primary_subsystem.atoms_for_interaction link_atoms = self.link_atoms primary_and_link_atoms = primary_atoms + link_atoms secondary_calculator = self.secondary_subsystem.calculator # The Pysic calculators in one simulation all share one CoreMirror # object that contains the data about the Atoms. This object should be # automatically updated if the number of atoms changes. This is however # not happening for some reason, so we temporarily force the updation here. TODO: # Find out why this is the case secondary_calculator.force_core_initialization = True E1 = secondary_calculator.get_potential_energy(link_atoms) E2 = secondary_calculator.get_potential_energy(primary_and_link_atoms) E3 = secondary_calculator.get_potential_energy(primary_atoms) secondary_calculator.force_core_initialization = False link_atom_correction_energy = -E1 - (E2 - E1 - E3) self.link_atom_correction_energy = link_atom_correction_energy self.timer.stop() return copy(self.link_atom_correction_energy) def calculate_link_atom_correction_forces(self): """Calculates the link atom correction forces defined as .. math:: F^\\text{link} = -\\nabla(-E^\\text{tot}_\\text{MM}(\\text{HL})-E^\\text{int}_\\text{MM}(\\text{PS, HL})) """ self.timer.start("Link atom correction forces") primary_atoms = self.primary_subsystem.atoms_for_interaction link_atoms = self.link_atoms primary_and_link_atoms = primary_atoms + link_atoms secondary_calculator = self.secondary_subsystem.calculator # The Pysic calculators in one simulation all share one CoreMirror # object that contains the data about the Atoms. This object should be # automatically updated if the number of atoms changes. This is however # not happening for some reason, so we temporarily force the updation here. TODO: # Find out why this is the case secondary_calculator.force_core_initialization = True # The force arrays from the individual subsystems need to be extended # to the size of the combined system forces = np.zeros((len(primary_and_link_atoms), 3)) primary_postfix = np.zeros((len(link_atoms), 3)) link_prefix = np.zeros((len(primary_atoms), 3)) # Calculate the force that binds the link atoms and primary atoms. We # don't need to calculate the force between the link atoms, although # the associated energy had to be calculated. F1 = secondary_calculator.get_forces(link_atoms) F2 = secondary_calculator.get_forces(primary_and_link_atoms) F3 = secondary_calculator.get_forces(primary_atoms) secondary_calculator.force_core_initialization = False forces += F2 forces -= np.concatenate((F3, primary_postfix), axis=0) forces -= np.concatenate((link_prefix, F1), axis=0) # Ignore the forces acting on link atoms, and negate the force # direction. The energy can't be ignored, force can forces = -forces[0 : self.n_primary, :] # Store the forces in a suitable numpy array that can be added to the # forces of the whole system link_atom_correction_forces = np.zeros((self.n_full, 3)) for sub_index in range(self.n_primary): full_index = self.primary_subsystem.reverse_index_map[sub_index] force = forces[sub_index, :] link_atom_correction_forces[full_index, :] = force self.link_atom_correction_forces = link_atom_correction_forces self.timer.stop() return copy(link_atom_correction_forces) def calculate_uncorrected_interaction_energy(self): """Calculates the interaction energy of a non-pbc system without the link atom correction. """ self.timer.start("Interaction") finite_energy = 0 primary = self.primary_subsystem.atoms_for_interaction secondary = self.secondary_subsystem.atoms_for_interaction combined = primary + secondary finite_energy = self.calculator.get_potential_energy(combined) self.uncorrected_interaction_energy = finite_energy self.timer.stop() return copy(finite_energy) def calculate_uncorrected_interaction_energy_pbc(self): """Calculates the interaction energy of a pbc system without the link atom correction. """ self.timer.start("Interaction (PBC)") pbc_energy = 0 primary = self.primary_subsystem.atoms_for_interaction secondary = self.secondary_subsystem.atoms_for_interaction combined = primary + secondary pbc_energy += self.pbc_calculator.get_potential_energy(combined) pbc_energy -= self.pbc_calculator.get_potential_energy(primary) pbc_energy -= self.pbc_calculator.get_potential_energy(secondary) self.uncorrected_interaction_energy = pbc_energy self.timer.stop() return copy(pbc_energy) def calculate_uncorrected_interaction_forces(self): """Calculates the interaction forces of a non-pbc system without the link atom correction. """ self.timer.start("Forces") primary = self.primary_subsystem.atoms_for_interaction.copy() secondary = self.secondary_subsystem.atoms_for_interaction.copy() combined_system = primary + secondary ## Forces due to finite coulomb potential and other pysic potentials forces = np.array(self.calculator.get_forces(combined_system)) self.uncorrected_interaction_forces = forces self.timer.stop() return copy(forces) def calculate_uncorrected_interaction_forces_pbc(self): """Calculates the interaction forces of a pbc system without the link atom correction. """ self.timer.start("Forces (PBC)") primary = self.primary_subsystem.atoms_for_interaction secondary = self.secondary_subsystem.atoms_for_interaction combined = primary + secondary primary_postfix = np.zeros((self.n_secondary, 3)) secondary_prefix = np.zeros((self.n_primary, 3)) # The force arrays from the individual subsystems need to be extended # to the size of the combined system forces = self.pbc_calculator.get_forces(combined) forces -= np.concatenate((self.pbc_calculator.get_forces(primary), primary_postfix), axis=0) forces -= np.concatenate((secondary_prefix, self.pbc_calculator.get_forces(secondary)), axis=0) self.uncorrected_interaction_forces = forces self.timer.stop() return copy(forces) def update_subsystem_charges(self): """Updates the charges in the subsystems involved in the interaction (if charge update is enabled in them). """ if self.info.coulomb_potential_enabled: self.primary_subsystem.update_charges() self.secondary_subsystem.update_charges() def get_interaction_energy(self): """Returns the total interaction energy which consists of the uncorrected energies and possible the link atom correction. Returns: float: the interaction energy """ # Try to update the charges self.update_subsystem_charges() interaction_energy = 0 # Calculate the interaction energies if self.has_interaction_potentials: if self.has_pbc: interaction_energy += self.calculate_uncorrected_interaction_energy_pbc() else: interaction_energy += self.calculate_uncorrected_interaction_energy() # Calculate the link atom correction energy if needed if self.info.link_atom_correction_enabled is True: interaction_energy += self.calculate_link_atom_correction_energy() self.interaction_energy = interaction_energy return copy(self.interaction_energy) def get_interaction_forces(self): """Return a numpy array of total 3D forces for each atom in the whole system. The forces consists of the uncorrected forces and possibly the link atom correction forces. The row index refers to the atom index in the original structure. Returns: numpy array: the forces for each atom in the full system """ # Try to update the charges self.update_subsystem_charges() interaction_forces = np.zeros((self.n_full, 3)) # Calculate the uncorrected interaction forces if self.has_interaction_potentials: if self.has_pbc: interaction_forces += self.calculate_uncorrected_interaction_forces_pbc() else: interaction_forces += self.calculate_uncorrected_interaction_forces() # Calculate the link atom correction forces if needed if self.info.link_atom_correction_enabled is True: interaction_forces += self.calculate_link_atom_correction_forces() self.interaction_forces = interaction_forces return copy(self.interaction_forces)
def __init__(self, atoms, info, index_map, reverse_index_map, n_atoms): """ Parameters: atoms: ASE Atoms The subsystem atoms. info: SubSystem object Contains all the information about the subsystem index_map: dictionary of int to int The keys are the atom indices in the full system, values are indices in the subssystem. reverse_index_map: dicitonary of int to int The keys are the atom indices in the subsystem, values are the keys in the full system. n_atoms: int Number of atoms in the full system. """ # Extract data from info self.name = info.name self.calculator = copy.copy(info.calculator) self.cell_size_optimization_enabled = info.cell_size_optimization_enabled self.cell_padding = info.cell_padding self.charge_calculation_enabled = info.charge_calculation_enabled self.charge_source = info.charge_source self.division = info.division self.gridrefinement = info.gridrefinement self.n_atoms = n_atoms self.atoms_for_interaction = atoms.copy() self.atoms_for_subsystem = atoms.copy() self.index_map = index_map self.reverse_index_map = reverse_index_map self.potential_energy = None self.forces = None self.density_grid = None self.pseudo_density = None self.link_atom_indices = [] self.timer = Timer([ "Bader charge calculation", "van Der Waals charge calculation", "Energy", "Forces", "Density grid update", "Cell minimization" ]) # The older ASE versions do not support get_initial_charges() try: charges = np.array(atoms.get_initial_charges()) except: charges = np.array(atoms.get_charges()) self.initial_charges = charges ## Can't enable charge calculation on non-DFT calculator self.dft_system = hasattr(self.calculator, "get_pseudo_density") if self.charge_calculation_enabled is True and not self.dft_system: error("Can't enable charge calculation on non-DFT calculator!") # If the cell size minimization flag has been enabled, then try to reduce the # cell size if self.cell_size_optimization_enabled: pbc = atoms.get_pbc() if pbc[0] or pbc[1] or pbc[2]: warn(("Cannot optimize cell size when periodic boundary" "condition have been enabled, disabling optimization."), 2) self.cell_size_optimization_enabled = False else: self.optimize_cell()
class SubSystemInternal(object): """A materialization of a SubSystem object. This class is materialised from a SubSystem, and should not be accessible to the end user. Attributes: name: string The unique name for this subsystem. calculator: ASE Calculator The calculator used. cell_size_optimization_enabled: bool - cell_padding: float - charge_calculation_enabled: bool - charge_source: string - division: string - gridrefinement: int - n_atoms: int Number of atoms in the full system. atoms_for_interaction: ASE Atoms The copy of subsystems atoms used in interaction calculations. atoms_for_subsystem: ASE Atoms The copy of subsystems atoms used in calculating subsystem energies etc. index_map: dictionary of int to int The keys are the atom indices in the full system, values are indices in the subssystem. reverse_index_map: dicitonary of int to int The keys are the atom indices in the subsystem, values are the keys in the full system. potential_energy: float - forces: numpy array - density_grid: numpy array Stored if spherical division is used in charge calculation. pseudo_density: numpy array - link_atom_indices: list - timer: :class:'~pysic.utility.timer.Timer' Used to keep track of time usage. """ def __init__(self, atoms, info, index_map, reverse_index_map, n_atoms): """ Parameters: atoms: ASE Atoms The subsystem atoms. info: SubSystem object Contains all the information about the subsystem index_map: dictionary of int to int The keys are the atom indices in the full system, values are indices in the subssystem. reverse_index_map: dicitonary of int to int The keys are the atom indices in the subsystem, values are the keys in the full system. n_atoms: int Number of atoms in the full system. """ # Extract data from info self.name = info.name self.calculator = copy.copy(info.calculator) self.cell_size_optimization_enabled = info.cell_size_optimization_enabled self.cell_padding = info.cell_padding self.charge_calculation_enabled = info.charge_calculation_enabled self.charge_source = info.charge_source self.division = info.division self.gridrefinement = info.gridrefinement self.n_atoms = n_atoms self.atoms_for_interaction = atoms.copy() self.atoms_for_subsystem = atoms.copy() self.index_map = index_map self.reverse_index_map = reverse_index_map self.potential_energy = None self.forces = None self.density_grid = None self.pseudo_density = None self.link_atom_indices = [] self.timer = Timer([ "Bader charge calculation", "van Der Waals charge calculation", "Energy", "Forces", "Density grid update", "Cell minimization" ]) # The older ASE versions do not support get_initial_charges() try: charges = np.array(atoms.get_initial_charges()) except: charges = np.array(atoms.get_charges()) self.initial_charges = charges ## Can't enable charge calculation on non-DFT calculator self.dft_system = hasattr(self.calculator, "get_pseudo_density") if self.charge_calculation_enabled is True and not self.dft_system: error("Can't enable charge calculation on non-DFT calculator!") # If the cell size minimization flag has been enabled, then try to reduce the # cell size if self.cell_size_optimization_enabled: pbc = atoms.get_pbc() if pbc[0] or pbc[1] or pbc[2]: warn(("Cannot optimize cell size when periodic boundary" "condition have been enabled, disabling optimization."), 2) self.cell_size_optimization_enabled = False else: self.optimize_cell() def optimize_cell(self): """Tries to optimize the cell of the subsystem so only the atoms in this subsystem fit in it with the defined padding to the edges. The new cell is always ortorhombic. """ self.timer.start("Cell minimization") padding = self.cell_padding x_min, y_min, z_min = self.atoms_for_subsystem[0].position x_max, y_max, z_max = self.atoms_for_subsystem[0].position for atom in self.atoms_for_subsystem: r = atom.position x = r[0] y = r[1] z = r[2] if x > x_max: x_max = x if y > y_max: y_max = y if z > z_max: z_max = z if x < x_min: x_min = x if y < y_min: y_min = y if z < z_min: z_min = z optimized_cell = np.array([ 2 * padding + x_max - x_min, 2 * padding + y_max - y_min, 2 * padding + z_max - z_min ]) self.atoms_for_subsystem.set_cell(optimized_cell) self.atoms_for_subsystem.center() self.timer.stop() def update_density_grid(self): """Precalculates a grid of 3D points for the charge calculation with van Der Waals radius. """ self.timer.start("Density grid update") calc = self.calculator atoms = self.atoms_for_subsystem # One calculation has to be made before the grid points can be asked grid_dim = calc.get_number_of_grid_points() nx = grid_dim[0] ny = grid_dim[1] nz = grid_dim[2] cell = atoms.get_cell() cx = cell[0] cy = cell[1] cz = cell[2] cux = cx / float((grid_dim[0] - 1)) cuy = cy / float((grid_dim[1] - 1)) cuz = cz / float((grid_dim[2] - 1)) # Create a 3D array of 3D points. Each point is a xyz-coordinate to a # position where the density has been calculated. density_grid = np.zeros((nx, ny, nz, 3)) for x in range(grid_dim[0]): for y in range(grid_dim[1]): for z in range(grid_dim[2]): r = x * cux + y * cuy + z * cuz density_grid[x, y, z, :] = r self.density_grid = density_grid self.timer.stop() def update_charges(self): """Updates the charges in the system. Depending on the value of self.division, calls either :meth:`~pysic.subsystem.SubSystemInternal.update_charges_bader`, or :meth:`~pysic.subsystem.SubSystemInternal.update_charges_van_der_waals` """ if self.charge_calculation_enabled: if self.division == "van Der Waals": self.update_charges_van_der_waals() if self.division == "Bader": self.update_charges_bader() def update_charges_bader(self): """Updates the charges in the atoms used for interaction with the Bader algorithm. This function uses an external Bader charge calculator from http://theory.cm.utexas.edu/henkelman/code/bader/. This tool is provided also in pysic/tools. Before using this function the bader executable directory has to be added to PATH. """ self.timer.start("Bader charge calculation") # The charges are calculated from the system that includes the link atoms bader_charges = get_bader_charges(self.atoms_for_subsystem, self.calculator, self.charge_source, self.gridrefinement) # Set the calculated charges to the interaction atoms. The call for # charges was changed between ASE 3.6 and 3.7. Ignore the link atoms # from the list of Bader charges n_limit = len(self.atoms_for_interaction) try: self.atoms_for_interaction.set_initial_charges( bader_charges[0:n_limit]) except: self.atoms_for_interaction.set_charges(bader_charges[0:n_limit]) self.timer.stop() def update_charges_van_der_waals(self): """Updates the atomic charges by using the electron density within a sphere of van Der Waals radius. The charge for each atom in the system is integrated from the electron density inside the van Der Waals radius of the atom in hand. The link atoms will affect the distribution of the electron density. """ self.timer.start("van Der Waals charge calculation") # Turn debugging on or off here debugging = False atoms_with_links = self.atoms_for_subsystem calc = self.calculator # The electron density is calculated from the system with link atoms. # This way the link atoms can modify the charge distribution calc.set_atoms(atoms_with_links) if self.charge_source == "pseudo": try: density = np.array(calc.get_pseudo_density()) except AttributeError: error("The DFT calculator on subsystem \"" + self.name + "\" doesn't provide pseudo density.") if self.charge_source == "all-electron": try: density = np.array( calc.get_all_electron_density(gridrefinement=1)) except AttributeError: error("The DFT calculator on subsystem \"" + self.name + "\" doesn't provide all electron density.") # Write the charge density as .cube file for VMD if debugging: write('nacl.cube', atoms_with_links, data=density) grid = self.density_grid if debugging: debug_list = [] # The link atoms are at the end of the list n_atoms = len(atoms_with_links) projected_charges = np.zeros((1, n_atoms)) for i_atom, atom in enumerate(atoms_with_links): r_atom = atom.position z = atom.number # Get the van Der Waals radius R = ase.data.vdw.vdw_radii[z] # Create a 3 x 3 x 3 x 3 array that can be used for vectorized # operations with the density grid r_atom_array = np.tile( r_atom, (grid.shape[0], grid.shape[1], grid.shape[2], 1)) diff = grid - r_atom_array # Numpy < 1.8 doesn't recoxnize axis argument on norm. This is a # workaround for diff = np.linalg.norm(diff, axis=3) diff = np.apply_along_axis(np.linalg.norm, 3, diff) indices = np.where(diff <= R) densities = density[indices] atom_charge = np.sum(densities) projected_charges[0, i_atom] = atom_charge if debugging: debug_list.append((atom, indices, densities)) #DEBUG: Visualize the grid and contributing grid points as atoms if debugging: d = Atoms() d.set_cell(atoms_with_links.get_cell()) # Visualize the integration spheres with atoms for point in debug_list: atom = point[0] indices = point[1] densities = point[2] d.append(atom) print "Atom: " + str(atom.symbol) + ", Density sum: " + str( np.sum(densities)) print "Density points included: " + str(len(densities)) for i in range(len(indices[0])): x = indices[0][i] y = indices[1][i] z = indices[2][i] a = Atom('H') a.position = grid[x, y, z, :] d.append(a) view(d) # Normalize the projected charges according to the electronic charge in # the whole system excluding the link atom to preserve charge neutrality atomic_numbers = np.array(atoms_with_links.get_atomic_numbers()) total_electron_charge = -np.sum(atomic_numbers) total_charge = np.sum(np.array(projected_charges)) projected_charges *= total_electron_charge / total_charge # Add the nuclear charges and initial charges projected_charges += atomic_numbers # Set the calculated charges to the atoms. The call for charges was # changed between ASE 3.6 and 3.7 try: self.atoms_for_interaction.set_initial_charges( projected_charges[0, :].tolist()) except: self.atoms_for_interaction.set_charges( projected_charges[0, :].tolist()) self.pseudo_density = density self.timer.stop() def get_potential_energy(self): """Returns the potential energy contained in this subsystem. """ # Update the cell size if minimization is on if self.cell_size_optimization_enabled: self.optimize_cell() # Ask the energy from the modified atoms (which include possible link # atoms) self.timer.start("Energy") self.potential_energy = self.calculator.get_potential_energy( self.atoms_for_subsystem) self.timer.stop() # Update the calculation grid if charge calculation with van Der Waals # division is enabled: if self.charge_calculation_enabled: if self.division == "van Der Waals": self.update_density_grid() return copy.copy(self.potential_energy) def get_forces(self): """Returns a 3D numpy array that contains forces for this subsystem. The returned array contains a row for each atom in the full system, but there is only an entry for the atoms in this subsystem. This makes it easier to calculate the total forces later on. """ # Update the cell size if minimization is on if self.cell_size_optimization_enabled: self.optimize_cell() # Calculate the forces self.timer.start("Forces") forces = self.calculator.get_forces(self.atoms_for_subsystem) self.timer.stop() # Ignore forces on link atoms, link atoms are at the end of the list forces = forces[0:len(self.atoms_for_interaction), :] # Store the forces in a suitable numpy array that can be added to the forces of # the whole system full_forces = np.zeros((self.n_atoms, 3)) for sub_index in range(len(self.atoms_for_interaction)): full_index = self.reverse_index_map[sub_index] force = forces[sub_index, :] full_forces[full_index, :] = force # Update the calculation grid if charge calculation with van Der Waals # division is enabled: if self.charge_calculation_enabled: if self.division == "van Der Waals": self.update_density_grid() self.forces = full_forces return copy.copy(self.forces) def get_pseudo_density(self): """Returns the electron pseudo density if available. """ if self.pseudo_density is not None: return copy.copy(self.pseudo_density) else: if hasattr(self.calculator, "get_pseudo_density"): return copy.copy( self.calculator.get_pseudo_density( self.atoms_for_subsystem)) else: warn( "The pseudo density for subsystem \"" + self.name + "\" is not available.", 2)
def __init__(self, atoms, info, index_map, reverse_index_map, n_atoms): """ Parameters: atoms: ASE Atoms The subsystem atoms. info: SubSystem object Contains all the information about the subsystem index_map: dictionary of int to int The keys are the atom indices in the full system, values are indices in the subssystem. reverse_index_map: dicitonary of int to int The keys are the atom indices in the subsystem, values are the keys in the full system. n_atoms: int Number of atoms in the full system. """ # Extract data from info self.name = info.name self.calculator = copy.copy(info.calculator) self.cell_size_optimization_enabled = info.cell_size_optimization_enabled self.cell_padding = info.cell_padding self.charge_calculation_enabled = info.charge_calculation_enabled self.charge_source = info.charge_source self.division = info.division self.gridrefinement = info.gridrefinement self.n_atoms = n_atoms self.atoms_for_interaction = atoms.copy() self.atoms_for_subsystem = atoms.copy() self.index_map = index_map self.reverse_index_map = reverse_index_map self.potential_energy = None self.forces = None self.density_grid = None self.pseudo_density = None self.link_atom_indices = [] self.timer = Timer([ "Bader charge calculation", "van Der Waals charge calculation", "Energy", "Forces", "Density grid update", "Cell minimization"]) # The older ASE versions do not support get_initial_charges() try: charges = np.array(atoms.get_initial_charges()) except: charges = np.array(atoms.get_charges()) self.initial_charges = charges ## Can't enable charge calculation on non-DFT calculator self.dft_system = hasattr(self.calculator, "get_pseudo_density") if self.charge_calculation_enabled is True and not self.dft_system: error("Can't enable charge calculation on non-DFT calculator!") # If the cell size minimization flag has been enabled, then try to reduce the # cell size if self.cell_size_optimization_enabled: pbc = atoms.get_pbc() if pbc[0] or pbc[1] or pbc[2]: warn(("Cannot optimize cell size when periodic boundary" "condition have been enabled, disabling optimization."), 2) self.cell_size_optimization_enabled = False else: self.optimize_cell()
class SubSystemInternal(object): """A materialization of a SubSystem object. This class is materialised from a SubSystem, and should not be accessible to the end user. Attributes: name: string The unique name for this subsystem. calculator: ASE Calculator The calculator used. cell_size_optimization_enabled: bool - cell_padding: float - charge_calculation_enabled: bool - charge_source: string - division: string - gridrefinement: int - n_atoms: int Number of atoms in the full system. atoms_for_interaction: ASE Atoms The copy of subsystems atoms used in interaction calculations. atoms_for_subsystem: ASE Atoms The copy of subsystems atoms used in calculating subsystem energies etc. index_map: dictionary of int to int The keys are the atom indices in the full system, values are indices in the subssystem. reverse_index_map: dicitonary of int to int The keys are the atom indices in the subsystem, values are the keys in the full system. potential_energy: float - forces: numpy array - density_grid: numpy array Stored if spherical division is used in charge calculation. pseudo_density: numpy array - link_atom_indices: list - timer: :class:'~pysic.utility.timer.Timer' Used to keep track of time usage. """ def __init__(self, atoms, info, index_map, reverse_index_map, n_atoms): """ Parameters: atoms: ASE Atoms The subsystem atoms. info: SubSystem object Contains all the information about the subsystem index_map: dictionary of int to int The keys are the atom indices in the full system, values are indices in the subssystem. reverse_index_map: dicitonary of int to int The keys are the atom indices in the subsystem, values are the keys in the full system. n_atoms: int Number of atoms in the full system. """ # Extract data from info self.name = info.name self.calculator = copy.copy(info.calculator) self.cell_size_optimization_enabled = info.cell_size_optimization_enabled self.cell_padding = info.cell_padding self.charge_calculation_enabled = info.charge_calculation_enabled self.charge_source = info.charge_source self.division = info.division self.gridrefinement = info.gridrefinement self.n_atoms = n_atoms self.atoms_for_interaction = atoms.copy() self.atoms_for_subsystem = atoms.copy() self.index_map = index_map self.reverse_index_map = reverse_index_map self.potential_energy = None self.forces = None self.density_grid = None self.pseudo_density = None self.link_atom_indices = [] self.timer = Timer([ "Bader charge calculation", "van Der Waals charge calculation", "Energy", "Forces", "Density grid update", "Cell minimization"]) # The older ASE versions do not support get_initial_charges() try: charges = np.array(atoms.get_initial_charges()) except: charges = np.array(atoms.get_charges()) self.initial_charges = charges ## Can't enable charge calculation on non-DFT calculator self.dft_system = hasattr(self.calculator, "get_pseudo_density") if self.charge_calculation_enabled is True and not self.dft_system: error("Can't enable charge calculation on non-DFT calculator!") # If the cell size minimization flag has been enabled, then try to reduce the # cell size if self.cell_size_optimization_enabled: pbc = atoms.get_pbc() if pbc[0] or pbc[1] or pbc[2]: warn(("Cannot optimize cell size when periodic boundary" "condition have been enabled, disabling optimization."), 2) self.cell_size_optimization_enabled = False else: self.optimize_cell() def optimize_cell(self): """Tries to optimize the cell of the subsystem so only the atoms in this subsystem fit in it with the defined padding to the edges. The new cell is always ortorhombic. """ self.timer.start("Cell minimization") padding = self.cell_padding x_min, y_min, z_min = self.atoms_for_subsystem[0].position x_max, y_max, z_max = self.atoms_for_subsystem[0].position for atom in self.atoms_for_subsystem: r = atom.position x = r[0] y = r[1] z = r[2] if x > x_max: x_max = x if y > y_max: y_max = y if z > z_max: z_max = z if x < x_min: x_min = x if y < y_min: y_min = y if z < z_min: z_min = z optimized_cell = np.array([2*padding + x_max - x_min, 2*padding + y_max - y_min, 2*padding + z_max - z_min]) self.atoms_for_subsystem.set_cell(optimized_cell) self.atoms_for_subsystem.center() self.timer.stop() def update_density_grid(self): """Precalculates a grid of 3D points for the charge calculation with van Der Waals radius. """ self.timer.start("Density grid update") calc = self.calculator atoms = self.atoms_for_subsystem # One calculation has to be made before the grid points can be asked grid_dim = calc.get_number_of_grid_points() nx = grid_dim[0] ny = grid_dim[1] nz = grid_dim[2] cell = atoms.get_cell() cx = cell[0] cy = cell[1] cz = cell[2] cux = cx / float((grid_dim[0] - 1)) cuy = cy / float((grid_dim[1] - 1)) cuz = cz / float((grid_dim[2] - 1)) # Create a 3D array of 3D points. Each point is a xyz-coordinate to a # position where the density has been calculated. density_grid = np.zeros((nx, ny, nz, 3)) for x in range(grid_dim[0]): for y in range(grid_dim[1]): for z in range(grid_dim[2]): r = x * cux + y * cuy + z * cuz density_grid[x, y, z, :] = r self.density_grid = density_grid self.timer.stop() def update_charges(self): """Updates the charges in the system. Depending on the value of self.division, calls either :meth:`~pysic.subsystem.SubSystemInternal.update_charges_bader`, or :meth:`~pysic.subsystem.SubSystemInternal.update_charges_van_der_waals` """ if self.charge_calculation_enabled: if self.division == "van Der Waals": self.update_charges_van_der_waals() if self.division == "Bader": self.update_charges_bader() def update_charges_bader(self): """Updates the charges in the atoms used for interaction with the Bader algorithm. This function uses an external Bader charge calculator from http://theory.cm.utexas.edu/henkelman/code/bader/. This tool is provided also in pysic/tools. Before using this function the bader executable directory has to be added to PATH. """ self.timer.start("Bader charge calculation") # The charges are calculated from the system that includes the link atoms bader_charges = get_bader_charges( self.atoms_for_subsystem, self.calculator, self.charge_source, self.gridrefinement) # Set the calculated charges to the interaction atoms. The call for # charges was changed between ASE 3.6 and 3.7. Ignore the link atoms # from the list of Bader charges n_limit = len(self.atoms_for_interaction) try: self.atoms_for_interaction.set_initial_charges(bader_charges[0:n_limit]) except: self.atoms_for_interaction.set_charges(bader_charges[0:n_limit]) self.timer.stop() def update_charges_van_der_waals(self): """Updates the atomic charges by using the electron density within a sphere of van Der Waals radius. The charge for each atom in the system is integrated from the electron density inside the van Der Waals radius of the atom in hand. The link atoms will affect the distribution of the electron density. """ self.timer.start("van Der Waals charge calculation") # Turn debugging on or off here debugging = False atoms_with_links = self.atoms_for_subsystem calc = self.calculator # The electron density is calculated from the system with link atoms. # This way the link atoms can modify the charge distribution calc.set_atoms(atoms_with_links) if self.charge_source == "pseudo": try: density = np.array(calc.get_pseudo_density()) except AttributeError: error("The DFT calculator on subsystem \"" + self.name + "\" doesn't provide pseudo density.") if self.charge_source == "all-electron": try: density = np.array(calc.get_all_electron_density(gridrefinement=1)) except AttributeError: error("The DFT calculator on subsystem \"" + self.name + "\" doesn't provide all electron density.") # Write the charge density as .cube file for VMD if debugging: write('nacl.cube', atoms_with_links, data=density) grid = self.density_grid if debugging: debug_list = [] # The link atoms are at the end of the list n_atoms = len(atoms_with_links) projected_charges = np.zeros((1, n_atoms)) for i_atom, atom in enumerate(atoms_with_links): r_atom = atom.position z = atom.number # Get the van Der Waals radius R = ase.data.vdw.vdw_radii[z] # Create a 3 x 3 x 3 x 3 array that can be used for vectorized # operations with the density grid r_atom_array = np.tile(r_atom, (grid.shape[0], grid.shape[1], grid.shape[2], 1)) diff = grid - r_atom_array # Numpy < 1.8 doesn't recoxnize axis argument on norm. This is a # workaround for diff = np.linalg.norm(diff, axis=3) diff = np.apply_along_axis(np.linalg.norm, 3, diff) indices = np.where(diff <= R) densities = density[indices] atom_charge = np.sum(densities) projected_charges[0, i_atom] = atom_charge if debugging: debug_list.append((atom, indices, densities)) #DEBUG: Visualize the grid and contributing grid points as atoms if debugging: d = Atoms() d.set_cell(atoms_with_links.get_cell()) # Visualize the integration spheres with atoms for point in debug_list: atom = point[0] indices = point[1] densities = point[2] d.append(atom) print "Atom: " + str(atom.symbol) + ", Density sum: " + str(np.sum(densities)) print "Density points included: " + str(len(densities)) for i in range(len(indices[0])): x = indices[0][i] y = indices[1][i] z = indices[2][i] a = Atom('H') a.position = grid[x, y, z, :] d.append(a) view(d) # Normalize the projected charges according to the electronic charge in # the whole system excluding the link atom to preserve charge neutrality atomic_numbers = np.array(atoms_with_links.get_atomic_numbers()) total_electron_charge = -np.sum(atomic_numbers) total_charge = np.sum(np.array(projected_charges)) projected_charges *= total_electron_charge/total_charge # Add the nuclear charges and initial charges projected_charges += atomic_numbers # Set the calculated charges to the atoms. The call for charges was # changed between ASE 3.6 and 3.7 try: self.atoms_for_interaction.set_initial_charges(projected_charges[0, :].tolist()) except: self.atoms_for_interaction.set_charges(projected_charges[0, :].tolist()) self.pseudo_density = density self.timer.stop() def get_potential_energy(self): """Returns the potential energy contained in this subsystem. """ # Update the cell size if minimization is on if self.cell_size_optimization_enabled: self.optimize_cell() # Ask the energy from the modified atoms (which include possible link # atoms) self.timer.start("Energy") self.potential_energy = self.calculator.get_potential_energy( self.atoms_for_subsystem) self.timer.stop() # Update the calculation grid if charge calculation with van Der Waals # division is enabled: if self.charge_calculation_enabled: if self.division == "van Der Waals": self.update_density_grid() return copy.copy(self.potential_energy) def get_forces(self): """Returns a 3D numpy array that contains forces for this subsystem. The returned array contains a row for each atom in the full system, but there is only an entry for the atoms in this subsystem. This makes it easier to calculate the total forces later on. """ # Update the cell size if minimization is on if self.cell_size_optimization_enabled: self.optimize_cell() # Calculate the forces self.timer.start("Forces") forces = self.calculator.get_forces(self.atoms_for_subsystem) self.timer.stop() # Ignore forces on link atoms, link atoms are at the end of the list forces = forces[0:len(self.atoms_for_interaction), :] # Store the forces in a suitable numpy array that can be added to the forces of # the whole system full_forces = np.zeros((self.n_atoms, 3)) for sub_index in range(len(self.atoms_for_interaction)): full_index = self.reverse_index_map[sub_index] force = forces[sub_index, :] full_forces[full_index, :] = force # Update the calculation grid if charge calculation with van Der Waals # division is enabled: if self.charge_calculation_enabled: if self.division == "van Der Waals": self.update_density_grid() self.forces = full_forces return copy.copy(self.forces) def get_pseudo_density(self): """Returns the electron pseudo density if available. """ if self.pseudo_density is not None: return copy.copy(self.pseudo_density) else: if hasattr(self.calculator, "get_pseudo_density"): return copy.copy(self.calculator.get_pseudo_density(self.atoms_for_subsystem)) else: warn("The pseudo density for subsystem \"" + self.name + "\" is not available.", 2)