Example #1
0
def test_energy_forces_stress():
    """
    To test that the calculator can produce correct energy and forces.  This
    is done by comparing the energy for an FCC argon lattice with an example
    model to the known value; the forces/stress returned by the model are
    compared to numerical estimates via finite difference.
    """
    import numpy as np
    from pytest import importorskip
    importorskip('kimpy')
    from ase.calculators.kim import KIM
    from ase.lattice.cubic import FaceCenteredCubic

    # Create an FCC atoms crystal
    atoms = FaceCenteredCubic(
        directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
        size=(1, 1, 1),
        symbol="Ar",
        pbc=(1, 0, 0),
        latticeconstant=3.0,
    )

    # Perturb the x coordinate of the first atom by less than the cutoff distance
    atoms.positions[0, 0] += 0.01

    calc = KIM("ex_model_Ar_P_Morse_07C")
    atoms.set_calculator(calc)

    # Get energy and analytical forces/stress from KIM model
    energy = atoms.get_potential_energy()
    forces = atoms.get_forces()
    stress = atoms.get_stress()

    # Previously computed energy for this configuration for this model
    energy_ref = 19.7196709065  # eV

    # Compute forces and virial stress numerically
    forces_numer = calc.calculate_numerical_forces(atoms, d=0.0001)
    stress_numer = calc.calculate_numerical_stress(atoms, d=0.0001, voigt=True)

    tol = 1e-6
    assert np.isclose(energy, energy_ref, tol)
    assert np.allclose(forces, forces_numer, tol)
    assert np.allclose(stress, stress_numer, tol)

    # This has been known to segfault
    atoms.set_pbc(True)
    atoms.get_potential_energy()
Example #2
0
def test_relax():
    """
    Test that a static relaxation that requires multiple neighbor list
    rebuilds can be carried out successfully.  This is verified by relaxing
    an icosahedral cluster of atoms and checking that the relaxed energy
    matches a known precomputed value for an example model.
    """
    import numpy as np
    from ase.cluster import Icosahedron
    from pytest import importorskip
    importorskip('kimpy')
    from ase.calculators.kim import KIM
    from ase.optimize import BFGS

    energy_ref = -0.5420939378624228  # eV

    # Create structure and calculator
    atoms = Icosahedron("Ar", latticeconstant=3.0, noshells=2)
    calc = KIM("ex_model_Ar_P_Morse_07C")
    atoms.calc = calc

    opt = BFGS(atoms, logfile=None)
    opt.run(fmax=0.05)

    assert np.isclose(atoms.get_potential_energy(), energy_ref)
Example #3
0
def test_multi_neighlist():
    """
    To test that the correct energy/forces/stress can be computed using a
    model that implements multiple cutoffs.  This is done by construct a 10
    Angstrom x 10 Angstrom x 10 Angstrom non-periodic cell filled with 15
    randomly positioned atoms and requesting tha tthe model compute the
    energy, forces, and virial stress.  The energy returned by the model is
    compared to a known precomputed value, while the forces and stress
    returned are compared to numerical estimates via finite difference.
    """
    import numpy as np
    from ase import Atoms

    from pytest import importorskip
    importorskip('kimpy')

    from ase.calculators.kim import KIM

    # Create random cluster of atoms
    positions = np.random.RandomState(34).rand(15, 3) * 10
    atoms = Atoms(
        "Ar" * 15, positions=positions, pbc=False, cell=[[10, 0, 0], [0, 10, 0], [0, 0, 10]]
    )

    calc = KIM("ex_model_Ar_P_Morse_MultiCutoff")
    atoms.set_calculator(calc)

    # Get energy and analytical forces/stress from KIM Model
    energy = atoms.get_potential_energy()
    forces = atoms.get_forces()
    stress = atoms.get_stress()

    # Previously computed energy for this configuration for this model
    energy_ref = 34.69963483186903  # eV

    # Compute forces and virial stress numerically
    forces_numer = calc.calculate_numerical_forces(atoms, d=0.0001)
    stress_numer = calc.calculate_numerical_stress(atoms, d=0.0001, voigt=True)

    tol = 1e-6
    assert np.isclose(energy, energy_ref, tol)
    assert np.allclose(forces, forces_numer, tol)
    assert np.allclose(stress, stress_numer, tol)
Example #4
0
def test_cutoff_skin():
    """
    To test that the calculator handles skin and cutoffs correctly.
    Specifically, note that the neighbor skin distance must be added onto
    both the model influence distance *and* each of the model cutoffs.  If
    the skin is not added onto the cutoffs, then an atom lying in between
    the largest cutoff and the skinned influence distance will not register
    as a neighbor if it hasn't already.

    The cutoff (and influence distance) for the model
    ex_model_Ar_P_Morse_07C is 8.15 Angstroms and the default skin distance
    when using the kimpy neighbor list library (which is the default when
    using a KIM portable model with this calculator) is 0.2 times the cutoff
    distance (1.63 Angstroms for this model).  Here, we construct a dimer
    with a separation falling just beyond the model cutoff but inside of the
    skinned influence distance.  We then compute the energy, which we expect
    to be zero in any case.  Next, we reduce the dimer separation by less
    than the skin distance so that the atoms fall within the cutoff of one
    another but without triggering a neighbor list rebuild.  If the atom had
    properly registered as a neighbor when it was outside of the cutoff but
    still inside of the skinned influence distance, then the energy in this
    case should be significantly far off from zero.  However, if the atom
    had failed to ever register as a neighbor, then we'll get zero once
    again.
    """
    import numpy as np
    from pytest import importorskip
    importorskip('kimpy')
    from ase.calculators.kim import KIM
    from ase import Atoms


    # Create calculator
    calc = KIM("ex_model_Ar_P_Morse_07C")

    # Create dimer with separation just beyond cutoff distance.  We *want*
    # these atoms to register as neighbors of one another since they fall
    # within the skinned influence distance of 9.78 Angstroms.
    model_cutoff = 8.15
    skin_distance = 0.2 * model_cutoff
    distance_orig = model_cutoff + 0.1 * skin_distance
    atoms = Atoms("Ar2", positions=[[0, 0, 0], [distance_orig, 0, 0]])
    atoms.set_calculator(calc)

    # Get energy -- should be zero
    e_outside_cutoff = atoms.get_potential_energy()

    # Now reduce the separation distance to something well within the model
    # cutoff -- should get something significantly non-zero
    atoms.positions[1, 0] -= 0.5 * skin_distance

    # Get new energy
    e_inside_cutoff = atoms.get_potential_energy()

    assert not np.isclose(e_outside_cutoff, e_inside_cutoff)
Example #5
0
model = "ex_model_Ar_P_Morse_07C"
model_cutoff = 8.15  # Angstroms

# Create a dimer centered in a small box that's small enough so that there will be
# padding atoms when we make it periodic
box_len = 0.5 * model_cutoff
dimer_separation = model_cutoff * 0.3

atoms = Atoms("Ar" * 2, cell=[[box_len, 0, 0], [0, box_len, 0], [0, 0, box_len]])

# Create calculator.  Either the kimpy neighbor list library or ASE's native neighbor
# lists should suffice to check this since update_kim_coords() belongs to their parent
# class, NeighborList.  Here, we'll use the default mode (kimpy neighbor list).
neigh_skin_ratio = 0.2
skin = neigh_skin_ratio * model_cutoff
calc = KIM(model, options={"neigh_skin_ratio": neigh_skin_ratio})
atoms.set_calculator(calc)

squeezed_energies_ref = {
    False: 5.784620078721877,  # finite
    True: 6.766293119162073,  # periodic
}

for pbc in [False, True]:

    # Reset dimer positions to original configuration
    set_positions_to_orig(atoms, box_len, dimer_separation)

    atoms.set_pbc(pbc)

    # Get potential energy so that it will get rid of "pbc" being in the system_changes.
Example #6
0
"""
Test that a static relaxation that requires multiple neighbor list
rebuilds can be carried out successfully.  This is verified by relaxing
an icosahedral cluster of atoms and checking that the relaxed energy
matches a known precomputed value for an example model.
"""
import numpy as np
from ase.cluster import Icosahedron
from ase.calculators.kim import KIM
from ase.optimize import BFGS

energy_ref = -0.5420939378624228  # eV

# Create structure and calculator
atoms = Icosahedron("Ar", latticeconstant=3.0, noshells=2)
calc = KIM("ex_model_Ar_P_Morse_07C")
atoms.set_calculator(calc)

opt = BFGS(atoms, logfile=None)
opt.run(fmax=0.05)

assert np.isclose(atoms.get_potential_energy(), energy_ref)
Example #7
0
def test_update_coords():
    """
    Check that the coordinates registered with the KIM API are updated
    appropriately when the atomic positions are updated.  This can go awry
    if the 'coords' attribute of the relevant NeighborList subclass is
    reassigned to a new memory location -- a problem which was indeed
    occurring at one point (see https://gitlab.com/ase/ase/merge_requests/1442)!
    """
    import numpy as np
    from ase import Atoms
    from pytest import importorskip
    importorskip('kimpy')
    from ase.calculators.kim import KIM

    def squeeze_dimer(atoms, d):
        """Squeeze the atoms together by the absolute distance ``d`` (Angstroms)
        """
        pos = atoms.get_positions()
        pos[0] += np.asarray([d, 0, 0])
        atoms.set_positions(pos)

    def set_positions_to_orig(atoms, box_len, dimer_separation):
        pos1 = np.asarray([box_len / 2.0, box_len / 2.0, box_len / 2.0
                           ]) - np.asarray([dimer_separation / 2.0, 0, 0])
        pos2 = np.asarray([box_len / 2.0, box_len / 2.0, box_len / 2.0
                           ]) + np.asarray([dimer_separation / 2.0, 0, 0])
        atoms.set_positions([pos1, pos2])

    # We know that ex_model_Ar_P_Morse_07C has a cutoff of 8.15 Angstroms
    model = "ex_model_Ar_P_Morse_07C"
    model_cutoff = 8.15  # Angstroms

    # Create a dimer centered in a small box that's small enough so that there will be
    # padding atoms when we make it periodic
    box_len = 0.5 * model_cutoff
    dimer_separation = model_cutoff * 0.3

    atoms = Atoms("Ar" * 2,
                  cell=[[box_len, 0, 0], [0, box_len, 0], [0, 0, box_len]])

    # Create calculator.  Either the kimpy neighbor list library or ASE's native neighbor
    # lists should suffice to check this since update_kim_coords() belongs to their parent
    # class, NeighborList.  Here, we'll use the default mode (kimpy neighbor list).
    neigh_skin_ratio = 0.2
    skin = neigh_skin_ratio * model_cutoff
    calc = KIM(model, options={"neigh_skin_ratio": neigh_skin_ratio})
    atoms.calc = calc

    squeezed_energies_ref = {
        False: 5.784620078721877,  # finite
        True: 6.766293119162073,  # periodic
    }

    for pbc in [False, True]:

        # Reset dimer positions to original configuration
        set_positions_to_orig(atoms, box_len, dimer_separation)

        atoms.set_pbc(pbc)

        # Get potential energy so that it will get rid of "pbc" being in the system_changes.
        # The only way that update_kim_coords is called is when system_changes
        # only contains "positions", as otherwise a neighbor list rebuild is triggered.
        atoms.get_potential_energy()

        # First squeeze the dimer together by a distance less than the skin and compute the
        # energy.  This doesn't trigger a neighbor list rebuild (which we avoid here because
        # we're only trying to test update_kim_coords, which is not called in the event that
        # a neighbor list rebuild is necessary).
        #
        # This will update the coordinate values in the ``coords`` attribute of the relevant
        # NeighborList subclass instance (see ase/calculators/kim/neighborlist.py) and,
        # since ``coords`` is pointing at the same memory the KIM API is reading the
        # coordinates from, the KIM Model will see the updated coordinates with no problem.
        # *However*, it's possible that after updating these values, the ``coords``
        # attribute is bound to a *new* object in memory if update_kim_coords is broken
        # somehow!
        squeeze_dimer(atoms, 0.2 * skin)
        atoms.get_potential_energy()

        # Now squeeze the dimer together by the same amount again, where the sum of this
        # squeeze and the last is still less than the skin distance so that we still avoid a
        # neighbor list rebuild.  If all is well, the `coords` attribute of the relevant
        # NeighborList subclass still points at the same location as before we did the
        # previous squeeze.  Thus, when we update the coordinates again, it should still be
        # updating the same coordinates being read by the KIM API, giving us the expected
        # value of the energy.  If update_kim_coords is broken, ``coords`` will already be
        # bound to a new object in memory while the KIM API is still looking at its previous
        # address to read coordinates.  This means that when we update the coordinates in
        # the ``coords`` value, the KIM API never sees them.  In fact, it's not even
        # guaranteed that the KIM API will read the same coordinates as before since the
        # memory where it's looking may have since been overwritten by some other python
        # objects.
        squeeze_dimer(atoms, 0.2 * skin)
        squeezed_energy = atoms.get_potential_energy()
        assert np.isclose(squeezed_energy, squeezed_energies_ref[pbc])
from ase.calculators.kim import KIM
from ase.lattice.cubic import FaceCenteredCubic

# Create an FCC atoms crystal
atoms = FaceCenteredCubic(
    directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
    size=(1, 1, 1),
    symbol="Ar",
    pbc=(1, 0, 0),
    latticeconstant=3.0,
)

# Perturb the x coordinate of the first atom by less than the cutoff distance
atoms.positions[0, 0] += 0.01

calc = KIM("ex_model_Ar_P_Morse_07C")
atoms.set_calculator(calc)

# Get energy and analytical forces/stress from KIM model
energy = atoms.get_potential_energy()
forces = atoms.get_forces()
stress = atoms.get_stress()

# Previously computed energy for this configuration for this model
energy_ref = 19.7196709065  # eV

# Compute forces and virial stress numerically
forces_numer = calc.calculate_numerical_forces(atoms, d=0.0001)
stress_numer = calc.calculate_numerical_stress(atoms, d=0.0001, voigt=True)

tol = 1e-6