Source code for pysic.interaction

#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""Defines classes used for creating and storing information about energetical
interactions between subsystems in a QM/MM hybrid calculation created with the
HybridCalculator-class.
"""
from pysic.utility.error import *
from ase import Atom, Atoms
from pysic import Pysic, CoulombSummation, Potential, ProductPotential
from pysic.interactions.comb import CombPotential
import numpy as np
from pysic.utility.timer import Timer
from copy import copy


#===============================================================================
[docs]class Interaction(object): """Used to store information about a interaction between subsystems. The end user can create and manipulate these objects when defining interactions between subsystems. The interactions between subsystems are added to the calculation by calling :meth:`~pysic.hybridcalculator.HybridCalculator.add_interaction` which adds a Interaction object to the calculation. When the HybridCalculator sees fit, the subsystem bindings are materialized by converting the stored Interactions to InteractionInternals. Attributes: primary: string Name of the primary subsystem. secondary: string Name of the secondary subsystem. links: list of tuples Contains a list of different link types. Each list item is a tuple with two items: the first is a list of link pairs, the second one is the CHL parameter for these links. electrostatic_parameters: dictionary Contains all the parameters used for creating a Coulomb potential. coulomb_potential_enabled: bool comb_potential_enabled: bool link_atom_correction_enabled: bool potentials: list of :class:`~pysic.interactions.local.Potential` """ def __init__(self, primary, secondary, ): """ Parameters: primary: string Name of the primary system. secondary: string Name of the secondary system. """ self.primary = primary self.secondary = secondary self.links = [] self.electrostatic_parameters = None self.coulomb_potential_enabled = False self.comb_potential_enabled = False self.link_atom_correction_enabled = True self.potentials = []
[docs] def set_potentials(self, potentials): """Used to set a list of additional Pysic potentials between the subsystems. Parameters: potentials: list of or single :class:`~pysic.interactions.local.Potential` """ if type(potentials) is list: self.potentials = potentials else: self.potentials = [potentials]
[docs] def add_potential(self, potential): """Used to add a Pysic potential between the subsystems. Parameters: potential: :class:`~pysic.interactions.local.Potential` """ self.potentials.append(potential)
[docs] def enable_coulomb_potential( self, epsilon=0.00552635, real_cutoff=None, k_cutoff=None, sigma=None): """Enables the electrical Coulomb interaction between the subsystems. Parameters: epsilon: float The vacumm permittivity. Has a default value from the literature, in Atomic units. r_cutoff: float Real space cutoff radius. Provide if system has periodic boundary conditions. k_cutoff: float Reciprocal space cutoff radius. Provide if system has periodic boundary conditions. sigma: float Ewald summation split parameter. Provide if system has periodic boundary conditions. """ self.coulomb_potential_enabled = True parameters = {'epsilon': epsilon} if sigma is not None: parameters['sigma'] = sigma if k_cutoff is not None: parameters['k_cutoff'] = k_cutoff if real_cutoff is not None: parameters['real_cutoff'] = real_cutoff self.electrostatic_parameters = parameters
[docs] def enable_comb_potential(self): """Enable the COMB potential between the subsystems. Valid for Si-Si and Si-O interactions. """ self.comb_potential_enabled = True
[docs]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
[docs] 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
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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)
[docs] 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()
[docs] 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)
[docs] 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)