Exemplo n.º 1
0
 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
Exemplo n.º 2
0
    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
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
    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
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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)
Exemplo n.º 7
0
    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()
Exemplo n.º 8
0
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)
Exemplo n.º 9
0
    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()
Exemplo n.º 10
0
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)